diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 0000000..5bc539d
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,47 @@
+{
+ "parser": "@typescript-eslint/parser",
+ "extends": [
+ "plugin:react/recommended",
+ "plugin:@typescript-eslint/recommended",
+ "prettier",
+ "plugin:react-hooks/recommended",
+ "plugin:prettier/recommended"
+ ],
+ "plugins": ["@typescript-eslint", "react", "prettier", "react-hooks"],
+ "parserOptions": {
+ "ecmaVersion": 11,
+ "sourceType": "module",
+ "ecmaFeatures": {
+ "jsx": true
+ }
+ },
+ "rules": {
+ "react-hooks/rules-of-hooks": "error",
+ "react-hooks/exhaustive-deps": "warn",
+ "comma-dangle": ["error", "only-multiline"],
+ "react/prop-types": "off",
+ "react/display-name": "off",
+ "@typescript-eslint/explicit-function-return-type": "off",
+ "prettier/prettier": ["error", { "endOfLine": "auto" }],
+ "@typescript-eslint/interface-name-prefix": "off",
+ "@typescript-eslint/ban-ts-ignore": "off",
+ "@typescript-eslint/explicit-module-boundary-types": "off",
+ "@typescript-eslint/no-empty-function": "off",
+ "@typescript-eslint/no-explicit-any": "error",
+ "@typescript-eslint/no-var-reqiures": "off",
+ "react/jsx-uses-react": "off",
+ "react/react-in-jsx-scope": "off"
+ },
+ "settings": {
+ "react": {
+ "pragma": "React",
+ "version": "detect"
+ }
+ },
+ "env": {
+ "browser": true,
+ "es6": true,
+ "jest": true
+ },
+ "root": true
+}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..93a5548
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,107 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# TypeScript v1 declaration files
+typings/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variables file
+.env
+.env.test
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+
+# Next.js build output
+.next
+
+# Nuxt.js build / generate output
+.nuxt
+dist
+
+# Gatsby files
+.cache/
+# Comment in the public line in if your project uses Gatsby and *not* Next.js
+# https://nextjs.org/blog/next-9-1#public-directory-support
+# public
+
+# vuepress build output
+.vuepress/dist
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# TernJS port file
+.tern-port
+
+# Package-lock
+package-lock.json
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000..df07c23
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,11 @@
+# Ignore artifacts:
+build
+coverage
+
+# Ignore all HTML files:
+*.html
+
+# Ignore all eslint and prettier files:
+*.json
+*.prettierignore
+*.eslintrc
diff --git a/.prettierrc.json b/.prettierrc.json
new file mode 100644
index 0000000..c651400
--- /dev/null
+++ b/.prettierrc.json
@@ -0,0 +1,9 @@
+{
+ "endOfLine": "auto",
+ "semi": true,
+ "singleQuote": true,
+ "tabWidth": 2,
+ "trailingComma": "es5",
+ "printWidth": 100,
+ "arrowParens": "always"
+}
diff --git a/SetUpBackend.md b/SetUpBackend.md
new file mode 100644
index 0000000..1bc3ba9
--- /dev/null
+++ b/SetUpBackend.md
@@ -0,0 +1,29 @@
+# Setup backend locally
+
+1. Go to https://github.com/vitaly-sazonov/kanban-rest
+2. Clone this repo to your pc
+3. Install git and NodeJS, if you don't have them
+4. Install Docker Desktop for Windows (or another OS), than reboot
+https://docs.docker.com/desktop/windows/install/
+5. Open Docker and install WSL 2 based engine, than reboot
+6. Open Docker and wait few seconds for the daemon to start. (you always need to run docker daemon for backend)
+7. Write in gitbash\console in kanban-rest directory "docker-compose up" to start backend
+
+# Deploy backend
+1. Open cmd\gitbash:
+git clone https://github.com/vitaly-sazonov/kanban-rest
+git switch source
+heroku create --region eu
+heroku addons:create heroku-postgresql:hobby-dev
+heroku config:set NPM_CONFIG_PRODUCTION=false
+heroku config:set LOG_CONSOLE=false
+heroku config:set LOG_ERR_LEVEL=warn
+heroku config:set LOG_INFO_LEVEL=info
+heroku config:set JWT_SECRET_KEY=secret-key
+heroku config:set SALT_SIZE=10
+heroku config:set USE_FASTIFY=true
+heroku git:remote -a bublikbackend
+git push heroku source:master
+
+# Deployed backend url
+https://bublikbackend.herokuapp.com/
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..d2ecb65
--- /dev/null
+++ b/package.json
@@ -0,0 +1,59 @@
+{
+ "name": "project-management-app",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@reduxjs/toolkit": "^1.8.1",
+ "@types/react-beautiful-dnd": "^13.1.2",
+ "jwt-decode": "^3.1.2",
+ "react": "^18.1.0",
+ "react-beautiful-dnd": "^13.1.0",
+ "react-dom": "^18.1.0",
+ "react-hook-form": "^7.31.1",
+ "react-redux": "^8.0.1",
+ "react-router-dom": "^6.3.0",
+ "react-scripts": "5.0.1"
+ },
+ "scripts": {
+ "start": "cross-env HTTPS=true react-scripts start",
+ "build": "react-scripts build",
+ "test": "react-scripts test",
+ "eject": "react-scripts eject"
+ },
+ "eslintConfig": {
+ "extends": [
+ "react-app",
+ "react-app/jest"
+ ]
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ },
+ "devDependencies": {
+ "@testing-library/jest-dom": "^5.16.4",
+ "@testing-library/react": "^13.1.1",
+ "@testing-library/user-event": "^14.1.1",
+ "@types/jest": "^27.4.1",
+ "@types/node": "^17.0.30",
+ "@types/react": "^18.0.8",
+ "@types/react-dom": "^18.0.3",
+ "@typescript-eslint/eslint-plugin": "^5.16.0",
+ "@typescript-eslint/parser": "^5.16.0",
+ "cross-env": "^7.0.3",
+ "eslint-config-prettier": "^8.5.0",
+ "eslint-config-react": "^1.1.7",
+ "eslint-plugin-prettier": "^4.0.0",
+ "prettier": "2.6.0",
+ "sass": "^1.51.0",
+ "typescript": "^4.6.4"
+ }
+}
diff --git a/public/_redirects b/public/_redirects
new file mode 100644
index 0000000..78f7f20
--- /dev/null
+++ b/public/_redirects
@@ -0,0 +1 @@
+/* /index.html 200
\ No newline at end of file
diff --git a/public/favicon.ico b/public/favicon.ico
new file mode 100644
index 0000000..bda0479
Binary files /dev/null and b/public/favicon.ico differ
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..86d126e
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+ Project Management App
+
+
+
+
+
+
diff --git a/public/manifest.json b/public/manifest.json
new file mode 100644
index 0000000..1f2f141
--- /dev/null
+++ b/public/manifest.json
@@ -0,0 +1,15 @@
+{
+ "short_name": "React App",
+ "name": "Create React App Sample",
+ "icons": [
+ {
+ "src": "favicon.ico",
+ "sizes": "64x64 32x32 24x24 16x16",
+ "type": "image/x-icon"
+ }
+ ],
+ "start_url": ".",
+ "display": "standalone",
+ "theme_color": "#000000",
+ "background_color": "#ffffff"
+}
diff --git a/public/robots.txt b/public/robots.txt
new file mode 100644
index 0000000..e9e57dc
--- /dev/null
+++ b/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/src/App.scss b/src/App.scss
new file mode 100644
index 0000000..e69de29
diff --git a/src/App.test.tsx b/src/App.test.tsx
new file mode 100644
index 0000000..7fe2a47
--- /dev/null
+++ b/src/App.test.tsx
@@ -0,0 +1,50 @@
+import { Provider } from 'react-redux';
+import { MemoryRouter } from 'react-router-dom';
+import { render, screen } from '@testing-library/react';
+import { store } from './app/store';
+
+import { App } from './App';
+import { Header } from './components/layout/header/Header';
+import { Footer } from './components/layout/footer/Footer';
+
+describe('Layout', () => {
+ // it('Header', () => {
+ // render(
+ //
+ //
+ //
+ // );
+
+ // const HEADER = screen.getByTestId('header');
+ // expect(HEADER).toBeInTheDocument();
+
+ // const HEADER_BTNS = screen.getAllByTestId('PrimaryButton');
+ // expect(HEADER_BTNS.length).toBe(2);
+ // });
+
+ it('Footer', () => {
+ render(
+
+
+
+ );
+
+ const FOOTER = screen.getByTestId('footer');
+ expect(FOOTER).toBeInTheDocument();
+ });
+});
+
+describe('Welcome page', () => {
+ it('Welcome page', () => {
+ render(
+
+
+
+
+
+ );
+
+ const WELCOME_PAGE = screen.getByTestId('welcomepage');
+ expect(WELCOME_PAGE).toBeInTheDocument();
+ });
+});
diff --git a/src/App.tsx b/src/App.tsx
new file mode 100644
index 0000000..cd6f912
--- /dev/null
+++ b/src/App.tsx
@@ -0,0 +1,68 @@
+import { FC } from 'react';
+import { Routes, Route } from 'react-router-dom';
+
+import { Layout } from './components/layout/Layout';
+import { WelcomePage } from './pages/welcomePage/WelcomePage';
+import { Boards } from './pages/boards/Boards';
+import { Board } from './pages/board/Board';
+import { NotFound } from './pages/notfound/NotFound';
+import { PATHS } from './shared/constants/routes';
+
+import './App.scss';
+import RequireAuth from './components/requireAuth/RequireAuth';
+import SignIn from './pages/signIn/SignIn';
+import SignUp from './pages/signUp/SignUp';
+import { UserProfile } from './pages';
+
+const App: FC = () => {
+ return (
+
+ }>
+ } />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+ } />
+
+
+ );
+};
+
+export { App };
diff --git a/src/app/RtkQuery.ts b/src/app/RtkQuery.ts
new file mode 100644
index 0000000..db895af
--- /dev/null
+++ b/src/app/RtkQuery.ts
@@ -0,0 +1,307 @@
+import {
+ BaseQueryFn,
+ FetchArgs,
+ createApi,
+ fetchBaseQuery,
+ FetchBaseQueryError,
+} from '@reduxjs/toolkit/query/react';
+import { saveTokenToLS } from '../features/ls-load-save';
+import { logoutUser } from '../reducers/auth';
+import { setEmptyUser } from '../reducers/userReducer';
+import { BoardType, SigninType, SignupType, ColumnType, TaskType, FileType } from './apiTypes';
+import { RootState } from './store';
+
+const baseQuery = fetchBaseQuery({
+ baseUrl: 'https://bublikbackend2.herokuapp.com/',
+ prepareHeaders: (headers, { getState }) => {
+ const token = (getState() as RootState).authStorage.userToken;
+
+ if (token) {
+ headers.set('authorization', `Bearer ${token}`);
+ }
+
+ return headers;
+ },
+});
+
+const baseQueryAuth: BaseQueryFn = async (
+ args,
+ api,
+ extraOptions
+) => {
+ const result = await baseQuery(args, api, extraOptions);
+ if (result.error && result.error.status === 401) {
+ api.dispatch(logoutUser());
+ api.dispatch(setEmptyUser());
+ saveTokenToLS('');
+ }
+ return result;
+};
+
+export const apiUser = createApi({
+ reducerPath: 'apiUser',
+ tagTypes: [
+ 'User',
+ 'UserUpdate',
+ 'TaskUpdate',
+ 'TaskDelete',
+ 'TaskPost',
+ 'Board',
+ 'Column',
+ 'Task',
+ ],
+ baseQuery: baseQueryAuth,
+ endpoints: (build) => ({
+ //USERS
+ getUsers: build.query({
+ query: () => `users`,
+ providesTags: ['UserUpdate', 'TaskUpdate'],
+ }),
+ getUserById: build.query({
+ query: (userId: string) => {
+ return {
+ url: `users/${userId}`,
+ };
+ },
+ providesTags: ['UserUpdate'],
+ }),
+ updateUser: build.mutation({
+ query(data: { userId: string; body: SignupType }) {
+ const { userId, body } = data;
+ return {
+ url: `users/${userId}`,
+ method: 'PUT',
+ body,
+ };
+ },
+ invalidatesTags: ['User', 'UserUpdate'],
+ }),
+ deleteUser: build.mutation({
+ query(userId: string) {
+ return {
+ url: `users/${userId}`,
+ method: 'DELETE',
+ };
+ },
+ invalidatesTags: ['User', 'UserUpdate'],
+ }),
+
+ //SIGN
+ signup: build.mutation({
+ query(body: SignupType) {
+ return {
+ url: `signup`,
+ method: 'POST',
+ body,
+ };
+ },
+ invalidatesTags: ['User', 'UserUpdate'],
+ }),
+ signin: build.mutation({
+ query(body: SigninType) {
+ return {
+ url: `signin`,
+ method: 'POST',
+ body,
+ };
+ },
+ invalidatesTags: ['User'],
+ }),
+
+ //BOARDS
+ getBoards: build.query({
+ query: () => `boards`,
+ providesTags: ['Board'],
+ }),
+ getBoardsById: build.query({
+ query: (boardId: string) => {
+ return {
+ url: `boards/${boardId}`,
+ };
+ },
+ providesTags: ['Board'],
+ }),
+ postBoard: build.mutation({
+ query(body: BoardType) {
+ return {
+ url: `boards`,
+ method: 'POST',
+ body,
+ };
+ },
+ invalidatesTags: ['Board'],
+ }),
+ updateBoard: build.mutation({
+ query(data: { boardId: string; body: BoardType }) {
+ const { boardId, body } = data;
+ return {
+ url: `boards/${boardId}`,
+ method: 'PUT',
+ body,
+ };
+ },
+ invalidatesTags: ['Board'],
+ }),
+ deleteBoard: build.mutation({
+ query(boardId: string) {
+ return {
+ url: `boards/${boardId}`,
+ method: 'DELETE',
+ };
+ },
+ invalidatesTags: ['Board'],
+ }),
+
+ //COLUMNS
+ getColumns: build.query({
+ query: (data: { boardId?: string }) => {
+ const { boardId } = data;
+ return {
+ url: `boards/${boardId}/columns`,
+ };
+ },
+ providesTags: ['Column'],
+ }),
+ getColumnById: build.query({
+ query: (data: { columnId: string; boardId: string }) => {
+ const { columnId, boardId } = data;
+ return {
+ url: `boards/${boardId}/columns/${columnId}`,
+ };
+ },
+ providesTags: ['Column'],
+ }),
+ postColumn: build.mutation({
+ query(data: { boardId: string; body: ColumnType }) {
+ const { boardId, body } = data;
+ return {
+ url: `boards/${boardId}/columns`,
+ method: 'POST',
+ body,
+ };
+ },
+ invalidatesTags: ['Column'],
+ }),
+ updateColumn: build.mutation({
+ query: (data: { columnId: string; boardId: string; body: ColumnType }) => {
+ const { columnId, boardId, body } = data;
+ return {
+ url: `boards/${boardId}/columns/${columnId}`,
+ method: 'PUT',
+ body,
+ };
+ },
+ invalidatesTags: ['Column'],
+ }),
+ deleteColumn: build.mutation({
+ query(data: { columnId: string; boardId: string }) {
+ const { columnId, boardId } = data;
+ return {
+ url: `boards/${boardId}/columns/${columnId}`,
+ method: 'DELETE',
+ };
+ },
+ invalidatesTags: ['Column'],
+ }),
+
+ //TASKS
+ getTasks: build.query({
+ query(data: { columnId: string; boardId: string }) {
+ const { columnId, boardId } = data;
+ return {
+ url: `boards/${boardId}/columns/${columnId}/tasks`,
+ };
+ },
+ providesTags: ['Task', 'TaskDelete', 'TaskPost', 'TaskUpdate'],
+ }),
+ getTaskById: build.query({
+ query: (data: { columnId: string; boardId: string; taskId: string }) => {
+ const { columnId, boardId, taskId } = data;
+ return {
+ url: `boards/${boardId}/columns/${columnId}/tasks/${taskId}`,
+ };
+ },
+ providesTags: ['Task'],
+ }),
+ postTask: build.mutation({
+ query(data: { boardId: string; columnId: string; body: TaskType }) {
+ const { boardId, columnId, body } = data;
+ return {
+ url: `boards/${boardId}/columns/${columnId}/tasks`,
+ method: 'POST',
+ body,
+ };
+ },
+ invalidatesTags: ['TaskPost'],
+ }),
+ updateTask: build.mutation({
+ query: (data: { columnId: string; boardId: string; taskId: string; task: TaskType }) => {
+ const { columnId, boardId, taskId, task } = data;
+ const body = { ...task, columnId, boardId };
+ return {
+ url: `boards/${boardId}/columns/${columnId}/tasks/${taskId}`,
+ method: 'PUT',
+ body,
+ };
+ },
+ invalidatesTags: ['TaskUpdate'],
+ }),
+ deleteTask: build.mutation({
+ query(data: { columnId: string; boardId: string; taskId: string }) {
+ const { columnId, boardId, taskId } = data;
+ return {
+ url: `boards/${boardId}/columns/${columnId}/tasks/${taskId}`,
+ method: 'DELETE',
+ };
+ },
+ invalidatesTags: ['TaskDelete'],
+ }),
+
+ //FILE
+ postFile: build.mutation({
+ query(body: FileType) {
+ return {
+ url: `file`,
+ method: 'POST',
+ body,
+ };
+ },
+ // invalidatesTags: ['File'],
+ }),
+ getFile: build.query({
+ query: (data: { taskId: string }) => {
+ const { taskId } = data;
+ return {
+ url: `file/${taskId}`,
+ };
+ },
+ // providesTags: ['File'],
+ }),
+ }),
+});
+
+export const {
+ useGetUsersQuery,
+ useGetUserByIdQuery,
+ useUpdateUserMutation,
+ useDeleteUserMutation,
+ useSigninMutation,
+ useSignupMutation,
+ useDeleteBoardMutation,
+ useGetBoardsByIdQuery,
+ useGetBoardsQuery,
+ usePostBoardMutation,
+ useUpdateBoardMutation,
+ useDeleteColumnMutation,
+ useGetColumnByIdQuery,
+ useGetColumnsQuery,
+ usePostColumnMutation,
+ useUpdateColumnMutation,
+ useGetTasksQuery,
+ useGetTaskByIdQuery,
+ useDeleteTaskMutation,
+ usePostTaskMutation,
+ useUpdateTaskMutation,
+ useGetFileQuery,
+ usePostFileMutation,
+} = apiUser;
diff --git a/src/app/apiTypes.ts b/src/app/apiTypes.ts
new file mode 100644
index 0000000..a8f1343
--- /dev/null
+++ b/src/app/apiTypes.ts
@@ -0,0 +1,32 @@
+export type SigninType = {
+ login: string;
+ password: string;
+};
+
+export type SignupType = {
+ name: string;
+ login: string;
+ password: string;
+};
+
+export type BoardType = {
+ title: string;
+ description: string;
+};
+
+export type ColumnType = {
+ title: string;
+ order?: number;
+};
+
+export type TaskType = {
+ title: string;
+ order?: number;
+ description: string;
+ userId: string;
+};
+
+export type FileType = {
+ taskId: string;
+ file: string;
+};
diff --git a/src/app/hooks.ts b/src/app/hooks.ts
new file mode 100644
index 0000000..520e84e
--- /dev/null
+++ b/src/app/hooks.ts
@@ -0,0 +1,6 @@
+import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
+import type { RootState, AppDispatch } from './store';
+
+// Use throughout your app instead of plain `useDispatch` and `useSelector`
+export const useAppDispatch = () => useDispatch();
+export const useAppSelector: TypedUseSelectorHook = useSelector;
diff --git a/src/app/store.ts b/src/app/store.ts
new file mode 100644
index 0000000..60b8198
--- /dev/null
+++ b/src/app/store.ts
@@ -0,0 +1,26 @@
+import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit';
+import { apiUser } from './RtkQuery';
+import authSlice from '../reducers/auth';
+import userSlice from '../reducers/userReducer';
+import langSlice from '../reducers/langReducer';
+import { localStorageMiddleware } from '../middleware/ls-middleware';
+
+export const store = configureStore({
+ reducer: {
+ authStorage: authSlice,
+ userStorage: userSlice,
+ langStorage: langSlice,
+ [apiUser.reducerPath]: apiUser.reducer,
+ },
+ middleware: (getDefaultMiddleware) =>
+ getDefaultMiddleware().concat(apiUser.middleware, localStorageMiddleware),
+});
+
+export type AppDispatch = typeof store.dispatch;
+export type RootState = ReturnType;
+export type AppThunk = ThunkAction<
+ ReturnType,
+ RootState,
+ unknown,
+ Action
+>;
diff --git a/src/components/boardColumn/BoardColumn.scss b/src/components/boardColumn/BoardColumn.scss
new file mode 100644
index 0000000..31ed959
--- /dev/null
+++ b/src/components/boardColumn/BoardColumn.scss
@@ -0,0 +1,107 @@
+@import '../../sass/Constants.scss';
+
+.board__column-container {
+ display: flex;
+ row-gap: $base;
+}
+
+.board__column {
+ display: flex;
+ flex-direction: column;
+ row-gap: $base * 2;
+ flex-basis: 26%;
+ min-width: $base * 50;
+ overflow: auto;
+ max-height: 65vh;
+ margin-right: 0.063rem;
+ padding: $base * 2.2 0 $base * 2.2 $base * 2.2;
+ border-top: 0.5rem solid $color-purple-09;
+ border-radius: $base * 2;
+ background-color: $color-white-07;
+ box-shadow: 0.063rem 0.063rem 0.063rem $color-dark-05, 0 0 0.06em $color-dark-05,
+ 0 0 0.06em $color-dark-05;
+ overflow-x: hidden;
+}
+
+.board__column-head {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ padding-bottom: $base * 2;
+ border-bottom: 0.063rem solid $color-dark-02;
+ min-width: 100%;
+ max-width: fit-content;
+}
+
+.cards__list__container {
+ @include scroll;
+ overflow: auto;
+ overflow-x: hidden;
+}
+
+.cards__list {
+ display: flex;
+ flex-direction: column;
+ row-gap: $base * 4;
+ padding: $base 0;
+}
+
+.task {
+ display: flex;
+ align-items: flex-start;
+ flex-direction: column;
+ padding: $base;
+ border: 0.063rem solid $color-dark-04;
+ border-radius: $base;
+ box-shadow: 0.063rem 0.063rem 0.063rem $color-dark-05, 0 0 0.06em $color-dark-05,
+ 0 0 0.06em $color-dark-05;
+ min-width: 100%;
+ max-width: fit-content;
+}
+
+.task-checkbox {
+ cursor: pointer;
+}
+
+.task-checkbox:checked + .task-text {
+ text-decoration: line-through;
+}
+
+.task-text {
+ position: relative;
+ font-size: $fs-l;
+ font-weight: $fw-semi-bold;
+ overflow: hidden;
+ overflow-wrap: break-word;
+ word-break: break-word;
+ cursor: pointer;
+ transition: $transition-01;
+ overflow: hidden;
+ max-height: 3rem;
+ -webkit-box-orient: vertical;
+ display: -webkit-box;
+ text-overflow: ellipsis;
+ -webkit-line-clamp: 2;
+
+ &::after {
+ position: absolute;
+ bottom: 0;
+ left: -100%;
+ content: '';
+ width: 100%;
+ height: 0.125rem;
+ background-color: $color-purple;
+ transition: $transition-01;
+ }
+
+ &:hover {
+ &::after {
+ left: 0;
+ transition: $transition-01;
+ }
+ }
+}
+
+.column__btn {
+ border: 0.063rem dotted $color-dark-03;
+}
diff --git a/src/components/boardColumn/BoardColumn.tsx b/src/components/boardColumn/BoardColumn.tsx
new file mode 100644
index 0000000..5050b87
--- /dev/null
+++ b/src/components/boardColumn/BoardColumn.tsx
@@ -0,0 +1,207 @@
+import React, { FC, RefObject, useEffect, useRef, useState } from 'react';
+
+import { ChangeTitleBtns, DeleteButton, TertiaryButton } from '../buttons';
+import { BoardColumnTitle, CardContainer, CardList, Modal } from '..';
+
+import './BoardColumn.scss';
+
+import { useDeleteColumnMutation, useGetTasksQuery, usePostTaskMutation } from '../../app/RtkQuery';
+
+import './BoardColumn.scss';
+import { useAppSelector } from '../../app/hooks';
+import { ColumnType } from '../../pages/board/Board';
+import { Draggable } from 'react-beautiful-dnd';
+import { localizationObj } from '../../features/localization';
+
+type BoardColumnProps = {
+ columnTitle: string;
+ boardId: string;
+ columnId: string;
+ order: number;
+ index: number;
+};
+
+type CardsState = {
+ id: string;
+ cardTitle: string;
+ complete: boolean;
+};
+
+export type TasksList = {
+ order: number;
+ title: string;
+ description: string;
+ id: string;
+ boardId: string;
+ columnId: string;
+ userId: string;
+ complete: boolean;
+};
+
+const BoardColumn: FC = ({ columnTitle, boardId, columnId, order, index }) => {
+ const columnRef = useRef() as RefObject;
+ const [taskTitle, setTaskTitle] = useState('');
+ const [taskDescription, setTaskDescription] = useState('');
+ const [isOpenCard, setIsOpenCard] = useState(false);
+ const { userId } = useAppSelector((state) => state.userStorage);
+ const [tasks, setTasks] = useState([]);
+ const { lang } = useAppSelector((state) => state.langStorage);
+ const { data, error, isLoading } = useGetTasksQuery({ columnId, boardId });
+ const [deleteColumn] = useDeleteColumnMutation();
+ const [activeModal, setActiveModal] = useState(false);
+ const [postTask] = usePostTaskMutation();
+ const [isTask, setIsTask] = useState(false);
+ const [isColumn, setIsColumn] = useState(false);
+
+ const removeColumn = async () => {
+ setActiveModal(false);
+ await deleteColumn({ boardId, columnId });
+ };
+
+ const addTask = async () => {
+ if (taskTitle.trim().length && taskDescription.trim().length) {
+ await postTask({
+ boardId,
+ columnId,
+ body: {
+ title: taskTitle,
+ description: taskDescription,
+ userId: userId,
+ },
+ });
+
+ setActiveModal(false);
+ setTaskTitle('');
+ setTaskDescription('');
+ }
+ };
+
+ useEffect(() => {
+ columnRef.current ? (columnRef.current.scrollTop = columnRef.current.scrollHeight) : null;
+ }, [data?.length, isOpenCard]);
+
+ useEffect(() => {
+ setTaskTitle('');
+ setIsOpenCard(false);
+ }, [data?.length]);
+
+ useEffect(() => {
+ if (data) {
+ setTasks([...data].sort((a: ColumnType, b: ColumnType) => a.order - b.order));
+ }
+ }, [data]);
+
+ useEffect(() => {
+ if (!activeModal) {
+ setIsColumn(false);
+ setIsTask(false);
+ setTaskTitle('');
+ setTaskDescription('');
+ }
+ }, [activeModal]);
+
+ return (
+ <>
+
+ {(provided, snapshot) => (
+ <>
+
+
+
+
+ {
+ setIsColumn(true);
+ setActiveModal(true);
+ }}
+ />
+
+
+
+
+
+ {
+ setIsTask(true);
+ setActiveModal(true);
+ }}
+ />
+
+
+
+
+
+ {isColumn && (
+ <>
+
+
+
{`${localizationObj[lang].doYouWantToDelete} '${columnTitle}' ?`}
+ setActiveModal(false)}
+ />
+
+ >
+ )}
+
+ {isTask && (
+ <>
+
+
+
{`${localizationObj[lang].addATitle}`}
+ ) =>
+ setTaskTitle(event?.target.value)
+ }
+ />
+ {`${localizationObj[lang].addADescription}`}
+ ) =>
+ setTaskDescription(event?.target.value)
+ }
+ />
+
+ {
+ setActiveModal(false);
+ setTaskTitle('');
+ setTaskDescription('');
+ }}
+ />
+
+ >
+ )}
+
+
+ >
+ )}
+
+ >
+ );
+};
+
+export default BoardColumn;
diff --git a/src/components/boardColumn/components/BoardColumnTitle/BoardColumnTitle.scss b/src/components/boardColumn/components/BoardColumnTitle/BoardColumnTitle.scss
new file mode 100644
index 0000000..3a84209
--- /dev/null
+++ b/src/components/boardColumn/components/BoardColumnTitle/BoardColumnTitle.scss
@@ -0,0 +1,25 @@
+@import '../../../../sass/Constants.scss';
+
+.board__column-input {
+ display: flex;
+ flex-direction: column;
+ row-gap: $base * 2;
+ padding: $base * 2 0;
+}
+
+.board__column-btns {
+ display: flex;
+ column-gap: $base;
+}
+
+.board__column-title {
+ overflow: hidden;
+ flex-basis: 85%;
+ -webkit-box-orient: vertical;
+ display: block;
+ display: -webkit-box;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ -webkit-line-clamp: 2;
+ word-break: break-word;
+}
diff --git a/src/components/boardColumn/components/BoardColumnTitle/BoardColumnTitle.tsx b/src/components/boardColumn/components/BoardColumnTitle/BoardColumnTitle.tsx
new file mode 100644
index 0000000..4b809f3
--- /dev/null
+++ b/src/components/boardColumn/components/BoardColumnTitle/BoardColumnTitle.tsx
@@ -0,0 +1,69 @@
+import { FC, useState } from 'react';
+
+import { useUpdateColumnMutation } from '../../../../app/RtkQuery';
+import { ChangeTitleBtns } from '../../../buttons';
+
+import './BoardColumnTitle.scss';
+
+interface BoardColumnTitleTypes {
+ columnTitle: string;
+ columnId: string;
+ boardId: string;
+ order: number;
+}
+
+const BoardColumnTitle: FC = ({ columnId, columnTitle, boardId, order }) => {
+ const [currentColumnTitle, setColumnTitle] = useState(columnTitle);
+ const [isOpenColumnTitle, setIsOpenColumnTitle] = useState(false);
+
+ const [updateColumn] = useUpdateColumnMutation();
+
+ const changeTitle = () => {
+ setIsOpenColumnTitle(true);
+ };
+
+ const submitColumnTitle = async () => {
+ if (currentColumnTitle.trim().length) {
+ setIsOpenColumnTitle(false);
+
+ await updateColumn({
+ columnId,
+ boardId,
+ body: { title: currentColumnTitle, order },
+ });
+ }
+ };
+
+ const cancelColumnTitle = () => {
+ setIsOpenColumnTitle(false);
+ setColumnTitle(columnTitle);
+ };
+
+ const handleColumnTitleValue = (event: React.ChangeEvent) => {
+ const currentInput = event.target as HTMLInputElement;
+ setColumnTitle(currentInput.value);
+ };
+
+ return (
+ <>
+ {isOpenColumnTitle ? (
+
+
+
+
+
+ ) : (
+
+ {columnTitle}
+
+ )}
+ >
+ );
+};
+
+export { BoardColumnTitle };
diff --git a/src/components/buttons/backButton/BackButton.scss b/src/components/buttons/backButton/BackButton.scss
new file mode 100644
index 0000000..0b605d8
--- /dev/null
+++ b/src/components/buttons/backButton/BackButton.scss
@@ -0,0 +1,70 @@
+@import '../../../sass/Constants.scss';
+
+.btn-back__wrapper {
+ @include flexCenter;
+ gap: $base;
+ width: $base * 20;
+ padding: $base;
+ background-color: $color-white;
+ border: none;
+ border-radius: $base;
+ box-shadow: 0.063rem 0.063rem 0.063rem $color-dark-05, 0 0 0.06em $color-dark-05,
+ 0 0 0.06em $color-dark-05;
+ transition: $transition-01;
+ cursor: pointer;
+
+ &:active {
+ transform: translate(0.063rem, 0.063rem);
+ transition: $transition-01;
+ }
+
+ @media (max-width: $breakpoint-lg-min) {
+ width: auto;
+ height: auto;
+ }
+
+ &:hover {
+ .btn-back {
+ filter: brightness(0.7);
+ }
+
+ .btn-new {
+ filter: brightness(0.7);
+ }
+ }
+}
+
+.btn-back-common {
+ width: $base * 5;
+ height: $base * 5;
+ background-color: transparent;
+ background-size: 100%;
+ background-repeat: no-repeat;
+ background-position: center;
+ border: none;
+
+ @media (max-width: $breakpoint-lg-min) {
+ width: $base * 7;
+ height: $base * 7;
+ background-size: 100%;
+ }
+}
+
+.btn-back {
+ background-image: url('../../../images/icons/back.svg');
+}
+
+.btn-new {
+ background-image: url('../../../images/icons/plus.svg');
+}
+
+.btn-back-description {
+ @include poppins;
+ width: min-content;
+ font-weight: $fw-semi-bold;
+ text-decoration: none;
+
+ @media (max-width: $breakpoint-lg-min) {
+ display: none;
+ }
+}
diff --git a/src/components/buttons/backButton/BackButton.tsx b/src/components/buttons/backButton/BackButton.tsx
new file mode 100644
index 0000000..58ef666
--- /dev/null
+++ b/src/components/buttons/backButton/BackButton.tsx
@@ -0,0 +1,28 @@
+import { FC } from 'react';
+
+import './BackButton.scss';
+
+interface BackButtonProps {
+ classNameWrapper: string;
+ className: string;
+ type: 'button';
+ description: string;
+ onClick?: () => void;
+}
+
+const BackButton: FC = ({
+ classNameWrapper,
+ className,
+ type,
+ description,
+ onClick,
+}) => {
+ return (
+
+ );
+};
+
+export { BackButton };
diff --git a/src/components/buttons/changeTitleBtns/ChangeTitleBtns.scss b/src/components/buttons/changeTitleBtns/ChangeTitleBtns.scss
new file mode 100644
index 0000000..844d28e
--- /dev/null
+++ b/src/components/buttons/changeTitleBtns/ChangeTitleBtns.scss
@@ -0,0 +1,58 @@
+@import '../../../sass/Constants.scss';
+
+.button-modal__wrapper {
+ @include poppins;
+ display: flex;
+ align-items: center;
+ column-gap: 0.125rem;
+ padding: 0 $base;
+ background-color: transparent;
+ border: none;
+ border-radius: $base;
+ box-shadow: 0.063rem 0.063rem 0.063rem $color-dark-05, 0 0 0.06em $color-dark-05,
+ 0 0 0.06em $color-dark-05;
+ cursor: pointer;
+ transition: $transition-01;
+
+ &:hover {
+ .button-modal {
+ filter: brightness(0.8);
+ transition: $transition-01;
+ }
+ }
+
+ &:active {
+ transform: translate(0.063rem, 0.063rem);
+ transition: $transition-01;
+ }
+}
+
+.button-modal {
+ width: $base * 6;
+ height: $base * 6;
+ background-size: 100%;
+ background-position: center;
+ background-repeat: no-repeat;
+ transition: $transition-01;
+}
+
+.space {
+ justify-content: space-between;
+}
+
+.button-modal-width {
+ flex-basis: 48%;
+}
+
+.button__submit {
+ background-image: url('../../../images/icons/submit.svg');
+}
+
+.button__cancel {
+ background-image: url('../../../images/icons/cancel.svg');
+}
+
+.button-modal__description {
+ font-size: $fs-m;
+ font-weight: $fw-semi-bold;
+}
diff --git a/src/components/buttons/changeTitleBtns/ChangeTitleBtns.tsx b/src/components/buttons/changeTitleBtns/ChangeTitleBtns.tsx
new file mode 100644
index 0000000..bf39dd3
--- /dev/null
+++ b/src/components/buttons/changeTitleBtns/ChangeTitleBtns.tsx
@@ -0,0 +1,28 @@
+import { FC, MouseEvent } from 'react';
+import { useAppSelector } from '../../../app/hooks';
+import { localizationObj } from '../../../features/localization';
+
+import './ChangeTitleBtns.scss';
+
+interface ChangeTitleBtnsProps {
+ onClickSubmit: (event: MouseEvent) => void;
+ onClickCancel: (event: MouseEvent) => void;
+}
+
+const ChangeTitleBtns: FC = ({ onClickSubmit, onClickCancel }) => {
+ const { lang } = useAppSelector((state) => state.langStorage);
+ return (
+
+
+
+
+ );
+};
+
+export { ChangeTitleBtns };
diff --git a/src/components/buttons/deleteButton/DeleteButton.scss b/src/components/buttons/deleteButton/DeleteButton.scss
new file mode 100644
index 0000000..54bbc82
--- /dev/null
+++ b/src/components/buttons/deleteButton/DeleteButton.scss
@@ -0,0 +1,21 @@
+@import '../../../sass/Constants.scss';
+
+.btn-delete {
+ min-width: $base * 5;
+ min-height: $base * 5;
+ background-color: transparent;
+ background-image: url('../../../images/icons/delete.svg');
+ background-size: 100%;
+ background-repeat: no-repeat;
+ background-position: center;
+ border: none;
+ cursor: pointer;
+
+ &:hover {
+ filter: brightness(0.7);
+ }
+}
+
+.btn-edit {
+ background-image: url('../../../images/icons/edit.svg');
+}
diff --git a/src/components/buttons/deleteButton/DeleteButton.tsx b/src/components/buttons/deleteButton/DeleteButton.tsx
new file mode 100644
index 0000000..792ab42
--- /dev/null
+++ b/src/components/buttons/deleteButton/DeleteButton.tsx
@@ -0,0 +1,16 @@
+import { FC, MouseEvent } from 'react';
+
+import './DeleteButton.scss';
+
+interface DeleteButtonProps {
+ className?: string;
+ type: 'button';
+ id?: string;
+ onClick?: (event: MouseEvent) => void;
+}
+
+const DeleteButton: FC = ({ className, type, id, onClick }) => {
+ return ;
+};
+
+export { DeleteButton };
diff --git a/src/components/buttons/header/PrimaryButton.scss b/src/components/buttons/header/PrimaryButton.scss
new file mode 100644
index 0000000..36afad7
--- /dev/null
+++ b/src/components/buttons/header/PrimaryButton.scss
@@ -0,0 +1,51 @@
+@import '../../../sass/Constants.scss';
+
+.btn {
+ @include poppins;
+ width: $base * 17;
+ height: $base * 7;
+ border: 0.063rem solid $color-yellow;
+ background-color: $color-white;
+ color: $color-white;
+ border-radius: $base * 4;
+ cursor: pointer;
+ transition: $transition;
+
+ &:hover {
+ border: 0.063rem solid $color-yellow;
+ transition: $transition;
+ }
+
+ &:active {
+ border: 0.125rem solid $color-orange;
+ transition: $transition;
+ }
+
+ &:focus {
+ box-shadow: 0 0 0 0.3rem $color-yellow-dark;
+ background-color: $color-yellow;
+ color: $color-white;
+ transition: $transition;
+ }
+}
+
+.btn-colored {
+ background-color: $color-yellow;
+ transition: $transition;
+
+ &:hover {
+ background-color: $color-orange;
+ transition: $transition;
+ }
+}
+
+.btn-bordered {
+ color: $color-yellow;
+ transition: $transition;
+
+ &:hover {
+ background-color: $color-yellow;
+ color: $color-white;
+ transition: $transition;
+ }
+}
diff --git a/src/components/buttons/header/PrimaryButton.tsx b/src/components/buttons/header/PrimaryButton.tsx
new file mode 100644
index 0000000..bde17c8
--- /dev/null
+++ b/src/components/buttons/header/PrimaryButton.tsx
@@ -0,0 +1,27 @@
+import { FC } from 'react';
+
+import './PrimaryButton.scss';
+
+interface PrimaryButtonProps {
+ dataTestId?: string;
+ type: 'submit' | 'reset' | 'button';
+ className: string;
+ description: string;
+ onClick?: () => void;
+}
+
+const PrimaryButton: FC = ({
+ dataTestId,
+ type,
+ className,
+ description,
+ onClick,
+}) => {
+ return (
+
+ );
+};
+
+export { PrimaryButton };
diff --git a/src/components/buttons/index.ts b/src/components/buttons/index.ts
new file mode 100644
index 0000000..473fdd9
--- /dev/null
+++ b/src/components/buttons/index.ts
@@ -0,0 +1,17 @@
+import { DeleteButton } from './deleteButton/DeleteButton';
+import { PrimaryButton } from './header/PrimaryButton';
+import { SecondaryButton } from './secondaryButton/SecondaryButton';
+import { TertiaryButton } from './tertiaryButton/TertiaryButton';
+import { BackButton } from './backButton/BackButton';
+import { Switcher } from './switcher/Switcher';
+import { ChangeTitleBtns } from './changeTitleBtns/ChangeTitleBtns';
+
+export {
+ DeleteButton,
+ PrimaryButton,
+ SecondaryButton,
+ TertiaryButton,
+ BackButton,
+ Switcher,
+ ChangeTitleBtns,
+};
diff --git a/src/components/buttons/secondaryButton/SecondaryButton.tsx b/src/components/buttons/secondaryButton/SecondaryButton.tsx
new file mode 100644
index 0000000..851ee0b
--- /dev/null
+++ b/src/components/buttons/secondaryButton/SecondaryButton.tsx
@@ -0,0 +1,18 @@
+import { FC } from 'react';
+
+interface SecondaryButtonProps {
+ className: string;
+ type: 'button';
+ description: string;
+ onClick: () => void;
+}
+
+const SecondaryButton: FC = ({ className, type, description, onClick }) => {
+ return (
+
+ );
+};
+
+export { SecondaryButton };
diff --git a/src/components/buttons/switcher/Switcher.scss b/src/components/buttons/switcher/Switcher.scss
new file mode 100644
index 0000000..f1272c7
--- /dev/null
+++ b/src/components/buttons/switcher/Switcher.scss
@@ -0,0 +1,70 @@
+@import '../../../sass/Constants.scss';
+
+.form__container {
+ font-weight: $fw-semi-bold;
+}
+
+.switcher__container {
+ display: flex;
+ align-items: center;
+ gap: $base;
+}
+
+.switch {
+ position: relative;
+ display: inline-block;
+ width: $base * 8;
+ height: $base * 5;
+}
+
+.switch input {
+ opacity: 0;
+ width: 0;
+ height: 0;
+}
+
+.slider {
+ position: absolute;
+ cursor: pointer;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: $color-yellow;
+ -webkit-transition: $transition;
+ transition: $transition;
+}
+
+.slider:before {
+ position: absolute;
+ content: '';
+ height: $base * 3;
+ width: $base * 3;
+ left: $base;
+ bottom: $base;
+ background-color: $color-white;
+ -webkit-transition: $transition;
+ transition: $transition;
+}
+
+input:checked + .slider {
+ background-color: $color-blue-light;
+}
+
+input:focus + .slider {
+ box-shadow: 0 0 0.063rem $color-blue-light;
+}
+
+input:checked + .slider:before {
+ -webkit-transform: translateX($base * 3);
+ -ms-transform: translateX($base * 3);
+ transform: translateX($base * 3);
+}
+
+.slider.round {
+ border-radius: $base * 7;
+}
+
+.slider.round:before {
+ border-radius: 50%;
+}
diff --git a/src/components/buttons/switcher/Switcher.tsx b/src/components/buttons/switcher/Switcher.tsx
new file mode 100644
index 0000000..237c5ca
--- /dev/null
+++ b/src/components/buttons/switcher/Switcher.tsx
@@ -0,0 +1,44 @@
+import { FC, useEffect, useState } from 'react';
+import { useAppDispatch, useAppSelector } from '../../../app/hooks';
+import { enLang, ruLang } from '../../../features/ls-load-save';
+import { setLang } from '../../../reducers/langReducer';
+
+import './Switcher.scss';
+
+interface SwitcherProps {
+ description: string;
+ type: string;
+ id: string;
+ name: string;
+}
+
+const Switcher: FC = ({ description, type, id, name }: SwitcherProps) => {
+ const dispatch = useAppDispatch();
+ const { lang } = useAppSelector((state) => state.langStorage);
+ const [isChecked, setIsChecked] = useState(lang === ruLang);
+ useEffect(() => {
+ dispatch(setLang(isChecked ? ruLang : enLang));
+ }, [isChecked]);
+
+ return (
+
+
{description}
+
+
{enLang}
+
+
{ruLang}
+
+
+ );
+};
+
+export { Switcher };
diff --git a/src/components/buttons/tertiaryButton/TertiaryButton.scss b/src/components/buttons/tertiaryButton/TertiaryButton.scss
new file mode 100644
index 0000000..2e9f241
--- /dev/null
+++ b/src/components/buttons/tertiaryButton/TertiaryButton.scss
@@ -0,0 +1,34 @@
+@import '../../../sass/Constants.scss';
+
+.button__tertiary {
+ @include poppins;
+ width: auto;
+ height: fit-content;
+ padding: $base $base * 2;
+ color: $color-dark-09;
+ background-color: $color-grey-07;
+ border-width: 0.063rem;
+ border-style: solid;
+ border-color: $color-dark-09;
+ border-radius: $base;
+ cursor: pointer;
+ transition: $transition;
+
+ &:hover {
+ color: $color-dark;
+ border-color: $color-dark;
+ filter: brightness(0.8);
+ transition: $transition;
+ }
+
+ @media (max-width: $breakpoint-md-min) {
+ padding: $base $base;
+ }
+}
+
+.button__tertiary-description {
+ @media (max-width: $breakpoint-md-min) {
+ width: max-content;
+ font-size: $fs-xs;
+ }
+}
diff --git a/src/components/buttons/tertiaryButton/TertiaryButton.tsx b/src/components/buttons/tertiaryButton/TertiaryButton.tsx
new file mode 100644
index 0000000..ede3900
--- /dev/null
+++ b/src/components/buttons/tertiaryButton/TertiaryButton.tsx
@@ -0,0 +1,31 @@
+import { FC } from 'react';
+
+import './TertiaryButton.scss';
+
+interface TertiaryButtonProps {
+ className: string;
+ type: 'button';
+ description: string;
+ isOpenCard?: boolean;
+ onClick: () => void;
+}
+
+const TertiaryButton: FC = ({
+ className,
+ type,
+ description,
+ isOpenCard,
+ onClick,
+}) => {
+ return (
+
+ );
+};
+
+export { TertiaryButton };
diff --git a/src/components/cardContainer/CardContainer.scss b/src/components/cardContainer/CardContainer.scss
new file mode 100644
index 0000000..48e74f7
--- /dev/null
+++ b/src/components/cardContainer/CardContainer.scss
@@ -0,0 +1,13 @@
+@import '../../sass/Constants.scss';
+
+.button__container {
+ display: flex;
+ align-items: center;
+ column-gap: $base;
+ width: 60%;
+ margin: 0 auto 0 0;
+}
+
+.hidden {
+ display: none;
+}
diff --git a/src/components/cardContainer/CardContainer.tsx b/src/components/cardContainer/CardContainer.tsx
new file mode 100644
index 0000000..1630ce6
--- /dev/null
+++ b/src/components/cardContainer/CardContainer.tsx
@@ -0,0 +1,49 @@
+import { FC, ChangeEvent } from 'react';
+
+import { Textarea } from '..';
+import { useAppSelector } from '../../app/hooks';
+import { localizationObj } from '../../features/localization';
+import { SecondaryButton, DeleteButton } from '../buttons';
+
+import './CardContainer.scss';
+
+interface CardContainerProps {
+ isOpenCard: boolean;
+ onClick: () => void;
+ cardTitle: string;
+ handleCardTitle: (event: ChangeEvent) => void;
+ addCard: () => void;
+}
+
+const CardContainer: FC = ({
+ isOpenCard,
+ onClick,
+ cardTitle,
+ handleCardTitle,
+ addCard,
+}) => {
+ const { lang } = useAppSelector((state) => state.langStorage);
+ return (
+
+ );
+};
+
+export { CardContainer };
diff --git a/src/components/cardItem/CardItem.scss b/src/components/cardItem/CardItem.scss
new file mode 100644
index 0000000..9aa767b
--- /dev/null
+++ b/src/components/cardItem/CardItem.scss
@@ -0,0 +1,72 @@
+@import '../../sass/Constants.scss';
+
+.board__task {
+ display: flex;
+ flex-direction: column;
+ row-gap: $base * 2;
+ width: 97%;
+ border-radius: $base;
+}
+
+.board__task-input {
+ display: flex;
+ align-items: flex-start;
+ flex-direction: column;
+ row-gap: $base * 2;
+ border: 0.063rem solid $color-dark-04;
+ border-radius: $base;
+}
+
+.board__task-btns {
+ display: flex;
+ column-gap: $base * 2;
+}
+
+.task__btns {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+}
+
+.task-owner {
+ font-size: $fs-xs;
+ font-weight: $fw-semi-bold;
+ cursor: pointer;
+}
+
+.task-owner-user {
+ font-weight: $fw-regular;
+}
+
+.users-list__item {
+ position: relative;
+ margin-left: auto;
+ cursor: grab;
+ width: fit-content;
+
+ &::after {
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ content: '';
+ width: 0%;
+ height: 0.125rem;
+ background-color: $color-purple;
+ transition: $transition-01;
+ }
+
+ &:hover {
+ color: $color-orange;
+ transition: $transition-01;
+ overflow: hidden;
+
+ &::after {
+ width: 100%;
+ }
+ }
+
+ @media (max-width: $breakpoint-sm-min) {
+ margin: auto;
+ }
+}
diff --git a/src/components/cardItem/CardItem.tsx b/src/components/cardItem/CardItem.tsx
new file mode 100644
index 0000000..c3177a3
--- /dev/null
+++ b/src/components/cardItem/CardItem.tsx
@@ -0,0 +1,322 @@
+import React, { FC, useEffect, useState } from 'react';
+import { Modal, Textarea } from '..';
+import { ChangeTitleBtns, DeleteButton, TertiaryButton } from '../buttons';
+
+import { useDeleteTaskMutation, useGetUsersQuery, useUpdateTaskMutation } from '../../app/RtkQuery';
+
+import './CardItem.scss';
+import { useAppSelector } from '../../app/hooks';
+import { Draggable } from 'react-beautiful-dnd';
+import { localizationObj } from '../../features/localization';
+import { createPortal } from 'react-dom';
+
+interface CardItemProps {
+ id: string;
+ cardTitle: string;
+ cardDescription: string;
+ columnId: string;
+ boardId: string;
+ userId: string;
+ order: number;
+ index: number;
+}
+
+const CardItem: FC = ({
+ id,
+ cardTitle,
+ cardDescription,
+ order,
+ columnId,
+ boardId,
+ userId: userOwnerId,
+ index,
+}) => {
+ const [taskTitle, setTaskTitle] = useState(cardTitle);
+ const [taskDescription, setTaskDescription] = useState(cardDescription);
+ const [updateTask] = useUpdateTaskMutation();
+ const [deleteTask] = useDeleteTaskMutation();
+ const [activeModal, setActiveModal] = useState(false);
+ const [isDeleteModal, setIsDeleteModal] = useState(false);
+ const [isUserChangeModal, setIsUserChangeModal] = useState(false);
+ const { userId } = useAppSelector((state) => state.userStorage);
+ const { lang } = useAppSelector((state) => state.langStorage);
+ const { data: users = [] } = useGetUsersQuery('');
+ const [userOwner, setUserOwner] = useState('');
+ const [isOpenTask, setIsOpenTask] = useState(false);
+ const container = document.getElementById('root')!;
+ const [isDisplayedTitleTextarea, setIsDisplayedTitleTextarea] = useState(false);
+ const [isDisplayedDescrTextarea, setIsDisplayedDescrTextarea] = useState(false);
+
+ const handleTaskTitleInput = (event: React.ChangeEvent) => {
+ setTaskTitle(event.target.value);
+ };
+
+ const handleTaskDescriptionInput = (event: React.ChangeEvent) => {
+ setTaskDescription(event.target.value);
+ };
+
+ const submitTitle = async () => {
+ setIsDisplayedTitleTextarea(false);
+
+ await updateTask({
+ columnId,
+ boardId,
+ taskId: id,
+ task: {
+ title: taskTitle,
+ order,
+ description: taskDescription,
+ userId: userOwnerId,
+ },
+ });
+ };
+
+ const submitDescr = async () => {
+ setIsDisplayedDescrTextarea(false);
+
+ await updateTask({
+ columnId,
+ boardId,
+ taskId: id,
+ task: {
+ title: taskTitle,
+ order,
+ description: taskDescription,
+ userId: userOwnerId,
+ },
+ });
+ };
+
+ const cancelTitle = () => {
+ setIsDisplayedTitleTextarea(false);
+ setTaskTitle(cardTitle);
+ };
+
+ const cancelDescr = () => {
+ setIsDisplayedDescrTextarea(false);
+ setTaskDescription(cardDescription);
+ };
+
+ const closeModal = () => {
+ setIsOpenTask(false);
+ setActiveModal(false);
+ setTaskDescription(cardDescription);
+ setTaskTitle(cardTitle);
+ };
+
+ const removeTask = async () => {
+ await deleteTask({
+ boardId,
+ columnId,
+ taskId: id,
+ });
+ setActiveModal(false);
+ };
+
+ const changeUser = async (newUserId: string) => {
+ if (!users.length) return;
+ await updateTask({
+ columnId,
+ boardId,
+ taskId: id,
+ task: {
+ title: cardTitle,
+ order,
+ description: cardDescription,
+ userId: newUserId,
+ },
+ });
+ setActiveModal(false);
+ };
+
+ const openTaskHandler = () => {
+ setIsOpenTask(true);
+ setActiveModal(true);
+ };
+
+ useEffect(() => {
+ if (!activeModal) {
+ setIsDeleteModal(false);
+ setIsUserChangeModal(false);
+ setIsOpenTask(false);
+ setIsDisplayedTitleTextarea(false);
+ setIsDisplayedDescrTextarea(false);
+ }
+ }, [activeModal]);
+
+ useEffect(() => {
+ if (users.length) {
+ const userName = users.find((user: { id: string; name: string }) => user.id === userOwnerId);
+ setUserOwner(userName ? userName.name : 'Deleted');
+ }
+ }, [users, userOwnerId]);
+
+ return (
+ <>
+
+ {(provided, snapshot) => (
+
+ <>
+
+
+ {cardTitle}
+
+ {
+ setIsUserChangeModal(true);
+ setActiveModal(true);
+ }}
+ >
+ {`${localizationObj[lang].author}: `}
+ {userOwner}
+
+
+
+ {
+ setActiveModal(true);
+ setIsDeleteModal(true);
+ }}
+ />
+
+
+ >
+
+ )}
+
+
+ {createPortal(
+
+ <>
+ {isDeleteModal && (
+
+
+
+
{`${localizationObj[lang].doYouWantToDelete} '${cardTitle}' ?`}
+ setActiveModal(false)}
+ />
+
+
+ )}
+ {isUserChangeModal && (
+
+
+
+
{`${localizationObj[lang].doYouWantToChangeUser}`}
+
+ {users.map((user: { id: string; name: string }) => (
+ - {
+ changeUser(user.id);
+ }}
+ >
+ {user.name}
+
+ ))}
+
+
+
+
+ )}
+ {isOpenTask && (
+
+
+ {`${localizationObj[lang].user}: `}
+ {userOwner}
+
+
+ {!isDisplayedTitleTextarea && (
+ <>
+
+ setIsDisplayedTitleTextarea(true)}
+ />
+
+ {`${localizationObj[lang].yourTask}: `}
+
+
+
{cardTitle}
+ >
+ )}
+ {isDisplayedTitleTextarea && (
+ <>
+
+
+
+ >
+ )}
+ {!isDisplayedDescrTextarea && (
+ <>
+
+ setIsDisplayedDescrTextarea(true)}
+ />
+ {`${localizationObj[lang].taskDescription}: `}
+
+
{cardDescription}
+ >
+ )}
+ {isDisplayedDescrTextarea && (
+ <>
+
+
+ >
+ )}
+
+
+
+ )}
+ >
+ ,
+ container
+ )}
+ >
+ );
+};
+
+export { CardItem };
diff --git a/src/components/cardList/CardList.tsx b/src/components/cardList/CardList.tsx
new file mode 100644
index 0000000..3d051e2
--- /dev/null
+++ b/src/components/cardList/CardList.tsx
@@ -0,0 +1,46 @@
+import { FC } from 'react';
+import { CardItem } from '..';
+import { Droppable } from 'react-beautiful-dnd';
+
+interface CardsState {
+ order: number;
+ complete: boolean;
+ title: string;
+ description: string;
+ id: string;
+ boardId: string;
+ columnId: string;
+ userId: string;
+}
+
+interface CardListProps {
+ tasks: CardsState[];
+ columnId: string;
+}
+
+const CardList: FC = ({ columnId, tasks }) => {
+ return (
+
+ {(provided) => (
+
+ {tasks.map((task, index) => {
+ return (
+
+ );
+ })}
+ {provided.placeholder}
+
+ )}
+
+ );
+};
+
+export { CardList };
diff --git a/src/components/index.ts b/src/components/index.ts
new file mode 100644
index 0000000..0b5b4aa
--- /dev/null
+++ b/src/components/index.ts
@@ -0,0 +1,25 @@
+import BoardColumn from './boardColumn/BoardColumn';
+import { BoardColumnTitle } from './boardColumn/components/BoardColumnTitle/BoardColumnTitle';
+import { CardContainer } from './cardContainer/CardContainer';
+import { CardItem } from './cardItem/CardItem';
+import { CardList } from './cardList/CardList';
+import { InputCheckbox } from './inputCheckbox/InputCheckbox';
+import { Layout } from './layout/Layout';
+import Modal from './modal/Modal';
+import RequireAuth from './requireAuth/RequireAuth';
+import { Textarea } from './textarea/Textarea';
+import { PreloaderSuspense } from './preloader';
+
+export {
+ BoardColumn,
+ CardContainer,
+ CardItem,
+ CardList,
+ InputCheckbox,
+ Layout,
+ Modal,
+ RequireAuth,
+ Textarea,
+ BoardColumnTitle,
+ PreloaderSuspense,
+};
diff --git a/src/components/inputCheckbox/InputCheckbox.tsx b/src/components/inputCheckbox/InputCheckbox.tsx
new file mode 100644
index 0000000..3f1ea2e
--- /dev/null
+++ b/src/components/inputCheckbox/InputCheckbox.tsx
@@ -0,0 +1,28 @@
+import { FC } from 'react';
+
+interface InputCheckboxProps {
+ className: string;
+ type: 'checkbox';
+ complete: boolean;
+ id?: string;
+ toggleCardComplete: (cardId: string) => void;
+}
+
+const InputCheckbox: FC = ({
+ className,
+ type,
+ complete,
+ id,
+ toggleCardComplete,
+}) => {
+ return (
+ toggleCardComplete(id ?? '')}
+ />
+ );
+};
+
+export { InputCheckbox };
diff --git a/src/components/layout/Layout.tsx b/src/components/layout/Layout.tsx
new file mode 100644
index 0000000..861daa6
--- /dev/null
+++ b/src/components/layout/Layout.tsx
@@ -0,0 +1,19 @@
+import { FC } from 'react';
+import { Outlet } from 'react-router-dom';
+
+import { Header } from './header/Header';
+import { Footer } from './footer/Footer';
+
+const Layout: FC = () => {
+ return (
+ <>
+
+
+
+
+
+ >
+ );
+};
+
+export { Layout };
diff --git a/src/components/layout/footer/Footer.scss b/src/components/layout/footer/Footer.scss
new file mode 100644
index 0000000..2590164
--- /dev/null
+++ b/src/components/layout/footer/Footer.scss
@@ -0,0 +1,118 @@
+@import '../../../sass/Constants.scss';
+
+.footer {
+ padding: $base * 2 0;
+ width: 100%;
+ height: auto;
+ background: $color-dark-02;
+}
+
+.footer__wrapper {
+ @include flexSpaceBetween;
+ column-gap: $base;
+}
+
+.footer__info {
+ @include flexCenter;
+ gap: $base * 2;
+ flex-wrap: wrap;
+
+ @media (max-width: $breakpoint-xs-min) {
+ gap: $base;
+ }
+}
+
+.footer__link {
+ display: block;
+ width: 100%;
+ height: inherit;
+ background-size: cover;
+}
+
+.footer__logo {
+ width: $base * 15;
+ height: $base * 5;
+}
+
+.footer__logo-link {
+ background-image: url('../../../images/icons/rs_school_logo.svg');
+ background-size: contain;
+ background-repeat: no-repeat;
+ background-position: center;
+ transition: $transition;
+
+ &:hover {
+ transform: scale(1.02);
+ transition: $transition;
+ }
+}
+
+.footer__team {
+ @include flexEnd;
+ flex-direction: column;
+ align-items: flex-end;
+ row-gap: $base;
+}
+
+.footer__title {
+ @include poppins;
+ @include disableSelection;
+ font-size: $fs-m;
+ font-weight: $fw-semi-bold;
+ text-align: end;
+ text-transform: uppercase;
+}
+
+.footer__contacts {
+ @include flexCenter;
+ column-gap: $base * 3;
+
+ @media (max-width: $breakpoint-xs-min) {
+ column-gap: $base * 2;
+ }
+}
+
+.footer__contact {
+ width: $base * 7;
+ height: $base * 7;
+ border-radius: 50%;
+ cursor: pointer;
+ overflow: hidden;
+ transform: scale(1);
+ transition: $transition;
+
+ &:hover {
+ box-shadow: 0 0 0 0.3rem $color-yellow-dark;
+ background-color: $color-yellow;
+ transform: scale(1.2);
+ transition: $transition;
+ }
+
+ @media screen and (max-width: $breakpoint-xs-min) {
+ width: $base * 5;
+ height: $base * 5;
+ }
+}
+
+.contact-1 {
+ background-image: url('../../../images/avatar-1.jpg');
+}
+
+.contact-2 {
+ background-image: url('../../../images/avatar-2.jpg');
+}
+
+.contact-3 {
+ background-image: url('../../../images/avatar-3.jpg');
+}
+
+.footer__year {
+ @include poppins;
+ @include disableSelection;
+ font-size: $fs-xxl;
+ font-weight: $fw-semi-bold;
+
+ @media (max-width: $breakpoint-xs-min) {
+ font-size: $fs-l;
+ }
+}
diff --git a/src/components/layout/footer/Footer.tsx b/src/components/layout/footer/Footer.tsx
new file mode 100644
index 0000000..31e61d9
--- /dev/null
+++ b/src/components/layout/footer/Footer.tsx
@@ -0,0 +1,57 @@
+import { FC } from 'react';
+import { FooterLink } from './FooterLink';
+import { Switcher } from '../../buttons';
+
+import './Footer.scss';
+import { useAppSelector } from '../../../app/hooks';
+import { localizationObj } from '../../../features/localization';
+
+const Footer: FC = () => {
+ const { lang } = useAppSelector((state) => state.langStorage);
+ return (
+
+ );
+};
+
+export { Footer };
diff --git a/src/components/layout/footer/FooterLink.tsx b/src/components/layout/footer/FooterLink.tsx
new file mode 100644
index 0000000..e6d3bb7
--- /dev/null
+++ b/src/components/layout/footer/FooterLink.tsx
@@ -0,0 +1,12 @@
+import { FC } from 'react';
+
+interface FooterItemProps {
+ href: string;
+ className: string;
+}
+
+const FooterLink: FC = ({ href, className }) => {
+ return ;
+};
+
+export { FooterLink };
diff --git a/src/components/layout/header/Header.scss b/src/components/layout/header/Header.scss
new file mode 100644
index 0000000..799f329
--- /dev/null
+++ b/src/components/layout/header/Header.scss
@@ -0,0 +1,135 @@
+@import '../../../sass/Constants.scss';
+
+.header {
+ position: sticky;
+ padding: $base * 2 0;
+ top: 0;
+ width: 100%;
+ height: auto;
+ backdrop-filter: blur($base * 2);
+ background: $color-grey-white;
+ transition: $transition;
+ z-index: 3;
+}
+
+.header__wrapper {
+ justify-content: space-between;
+}
+
+.header__logo__wrapper {
+ display: flex;
+ align-items: center;
+ gap: $base;
+
+ .button__tertiary {
+ @media (max-width: $breakpoint-sm-min) {
+ width: $base * 7;
+ height: $base * 7;
+ background-color: transparent;
+ background-image: url('../../../images/icons/plus.svg');
+ background-size: 100%;
+ background-position: center;
+ background-repeat: no-repeat;
+ border: none;
+ }
+ }
+
+ .button__tertiary-description {
+ @media (max-width: $breakpoint-sm-min) {
+ display: none;
+ }
+ }
+}
+
+.boards-logo__wrapper {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: transparent;
+ cursor: pointer;
+
+ &:hover {
+ .boards-logo {
+ filter: brightness(0.9);
+ transition: $transition;
+ }
+
+ .user-logo {
+ filter: brightness(0.8);
+ transition: $transition;
+ }
+ }
+}
+
+.boards-logo {
+ width: $base * 5;
+ height: $base * 5;
+ background-color: transparent;
+ background-image: url('../../../images/icons/boards.svg');
+ background-size: 100%;
+ background-repeat: no-repeat;
+ background-position: center;
+ border: none;
+ transition: $transition;
+
+ @media (max-width: $breakpoint-xs-min) {
+ width: $base * 7;
+ height: $base * 7;
+ background-size: 100%;
+ }
+}
+
+.user-logo {
+ background-image: url('../../../images/icons/user.svg');
+}
+
+.boards-logo-description {
+ color: $color-dark;
+ font-weight: $fw-semi-bold;
+
+ @media (max-width: $breakpoint-md-min) {
+ width: max-content;
+ font-size: $fs-xs;
+ }
+
+ @media (max-width: $breakpoint-xs-min) {
+ display: none;
+ }
+}
+
+.header-scrolled {
+ padding: $base 0;
+ background: $color-grey-07;
+ box-shadow: 0 $base * 0.1 $base * 3 $color-dark-02;
+}
+
+.header__logo {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: $base * 20;
+ height: 100%;
+ background-image: url('../../../images/icons/logo.svg');
+ background-size: 90%;
+ background-position: center;
+ background-repeat: no-repeat;
+ cursor: pointer;
+ transition: $transition;
+
+ @media (max-width: $breakpoint-sm-min) {
+ width: $base * 10;
+ background-image: url('../../../images/icons/logo-small.png');
+ background-size: 70%;
+ transition: $transition;
+ }
+}
+
+.header__buttons {
+ display: flex;
+ align-items: center;
+ column-gap: $base * 2;
+
+ @media (max-width: $breakpoint-lg-min) {
+ column-gap: $base;
+ }
+}
diff --git a/src/components/layout/header/Header.tsx b/src/components/layout/header/Header.tsx
new file mode 100644
index 0000000..034dc74
--- /dev/null
+++ b/src/components/layout/header/Header.tsx
@@ -0,0 +1,179 @@
+import { FC, useEffect, useState } from 'react';
+import { Link, useLocation, useNavigate } from 'react-router-dom';
+import { PATHS } from '../../../shared/constants/routes';
+import { useAppDispatch, useAppSelector } from '../../../app/hooks';
+import { saveTokenToLS } from '../../../features/ls-load-save';
+import { logoutUser } from '../../../reducers/auth';
+import decodeUserId from '../../../features/decodeUserId';
+import { useGetUserByIdQuery, usePostBoardMutation } from '../../../app/RtkQuery';
+import { setEmptyUser, setUserData } from '../../../reducers/userReducer';
+import { ChangeTitleBtns, PrimaryButton, TertiaryButton } from '../../buttons';
+
+import './Header.scss';
+import { localizationObj } from '../../../features/localization';
+import Modal from '../../modal/Modal';
+
+const Header: FC = () => {
+ const [postBoard] = usePostBoardMutation();
+ const [scrolledPage, isScrolledPage] = useState(false);
+ const [activeModal, setActiveModal] = useState(false);
+ const [boardTitle, setBoardTitle] = useState('');
+ const [boardDescription, setBoardDescription] = useState('');
+ const body = window.document.body as HTMLBodyElement;
+ const heightScrollTop = 1;
+ const navigate = useNavigate();
+ const location = useLocation();
+ const { userToken } = useAppSelector((state) => state.authStorage);
+ const { userName } = useAppSelector((state) => state.userStorage);
+ const dispatch = useAppDispatch();
+ const { lang } = useAppSelector((state) => state.langStorage);
+
+ const listenScrollEvent = () => {
+ body.scrollTop > heightScrollTop ? isScrolledPage(true) : isScrolledPage(false);
+ };
+
+ const userId = decodeUserId(userToken); // receive userId
+ const { data } = useGetUserByIdQuery(userId);
+
+ const addNewBoard = async () => {
+ if (boardTitle.trim().length && boardDescription.trim().length) {
+ await postBoard({ title: boardTitle, description: boardDescription });
+ setActiveModal(false);
+ setBoardTitle('');
+ setBoardDescription('');
+ }
+ };
+
+ useEffect(() => {
+ if (data && 'name' in data && 'id' in data && 'login' in data) {
+ dispatch(
+ setUserData({
+ userName: data.name,
+ userId: data.id,
+ userLogin: data.login,
+ })
+ );
+ }
+ }, [userId, data]);
+
+ useEffect(() => {
+ body.addEventListener('scroll', listenScrollEvent);
+ return () => body.removeEventListener('scroll', listenScrollEvent);
+ }, [scrolledPage]);
+
+ useEffect(() => {
+ if (!activeModal) {
+ setBoardTitle('');
+ setBoardDescription('');
+ }
+ }, [activeModal]);
+
+ return (
+ <>
+
+
+
+
+ {userToken && (
+ <>
+
+
+
+
+ {localizationObj[lang].boardsHeader}
+
+
+
+ {location.pathname === `${PATHS.boards}` && (
+
setActiveModal(true)}
+ />
+ )}
+ >
+ )}
+
+
+ {!userToken && (
+ <>
+
navigate(PATHS.signIn, { replace: true })}
+ />
+ navigate(PATHS.signUp, { replace: true })}
+ />
+ >
+ )}
+ {userToken && (
+ <>
+ navigate(PATHS.userProfile)}
+ >
+
+
{userName}
+
+ {
+ dispatch(logoutUser());
+ dispatch(setEmptyUser());
+ saveTokenToLS('');
+ }}
+ />
+ >
+ )}
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export { Header };
diff --git a/src/components/modal/Modal.scss b/src/components/modal/Modal.scss
new file mode 100644
index 0000000..0d79dab
--- /dev/null
+++ b/src/components/modal/Modal.scss
@@ -0,0 +1,218 @@
+@import '../../sass/Constants.scss';
+
+.modal {
+ position: fixed;
+ top: 0;
+ left: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100vw;
+ height: 100vh;
+ background-color: rgba(0, 0, 0, 0.4);
+ opacity: 0;
+ transition: $transition;
+ pointer-events: none;
+ z-index: 50;
+
+ &.active {
+ opacity: 1;
+ pointer-events: all;
+ }
+
+ .modal__content {
+ border-radius: $base * 3;
+ background-color: rgb(255, 255, 255);
+ transform: scale(0.5);
+ width: 60%;
+ height: 60vh;
+ transition: $transition all;
+
+ @media (max-width: $breakpoint-xl-min) {
+ width: 76vw;
+ }
+
+ @media (max-width: $breakpoint-md-min) {
+ width: 90vw;
+ }
+
+ &.active {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ opacity: 1;
+ pointer-events: all;
+ transform: scale(1);
+ }
+ }
+
+ .modal__wrapper {
+ display: flex;
+ justify-content: space-between;
+ width: 100%;
+ height: 100%;
+ }
+
+ .modal__img {
+ flex-basis: 50%;
+ height: auto;
+ background-size: cover;
+ background-color: rgb(255, 255, 255);
+ background-position: center;
+ border-radius: $base * 3 0 0 $base * 3;
+
+ @media (max-width: $breakpoint-md-min) {
+ flex-basis: 55%;
+ }
+
+ @media (max-width: $breakpoint-sm-min) {
+ flex-basis: 0%;
+ }
+ }
+
+ .modal__img-delete {
+ background-image: url('../../images/confirmation.jpg');
+ background-repeat: no-repeat;
+
+ @media (max-width: $breakpoint-sm-min) {
+ background: rgb(255, 255, 255);
+ }
+ }
+
+ .modal__img-add {
+ background-image: url('../../images/modal/create.jpg');
+ background-repeat: no-repeat;
+
+ @media (max-width: $breakpoint-sm-min) {
+ background: rgb(255, 255, 255);
+ }
+ }
+
+ .modal__img-user-change {
+ background-image: url('../../images/modal/changeUser.jpg');
+ background-repeat: no-repeat;
+
+ @media (max-width: $breakpoint-sm-min) {
+ background: rgb(255, 255, 255);
+ }
+ }
+
+ .modal__text {
+ display: flex;
+ align-items: flex-end;
+ flex-direction: column;
+ row-gap: $base * 3;
+ padding: $base * 19 $base * 4 $base * 4;
+ flex-basis: 45%;
+ text-align: end;
+ word-break: break-word;
+
+ h2 {
+ overflow: hidden;
+ line-height: 2rem;
+ -webkit-box-orient: vertical;
+ display: -webkit-box;
+ text-overflow: ellipsis;
+ -webkit-line-clamp: 5;
+ }
+
+ input {
+ padding: $base * 2;
+ width: 90%;
+ height: $base * 6;
+ border-radius: $base * 2;
+ border: $base * 0.2 solid $color-purple;
+ }
+
+ @media (max-width: $breakpoint-sm-min) {
+ flex-basis: 100%;
+ align-items: center;
+ text-align: center;
+ }
+ }
+
+ .modal__btns {
+ display: flex;
+ column-gap: $base * 3;
+ }
+
+ .modal__close {
+ position: absolute;
+ top: $base;
+ right: $base * 2;
+ font-size: $base * 6;
+ transition: $transition linear;
+ cursor: pointer;
+
+ &:hover {
+ transform: rotate(90deg);
+ }
+
+ &:active {
+ transform: scale(0.8);
+ }
+ }
+
+ .modal__tasks {
+ padding: $base * 2 $base * 5 $base * 4;
+ flex-direction: column;
+ align-items: flex-start;
+ overflow-y: auto;
+ @include scroll;
+ }
+
+ .modal__tasks > .modal__text {
+ flex-basis: 100%;
+ align-items: flex-start;
+ padding: 0;
+ width: 100%;
+ text-align: initial;
+ }
+
+ .modal__tasks > .modal__tasks-text {
+ border: $base * 0.2 solid $color-purple;
+ border-radius: $base;
+ margin: $base * 10 0;
+ padding: $base * 2;
+ }
+
+ .modal__text-wrapper {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ column-gap: $base * 3;
+ width: 100%;
+ }
+
+ .modal__text > .boards__item-input {
+ width: 98%;
+ }
+
+ .modal__tasks-textarea {
+ width: 98%;
+ }
+
+ .modal__tasks-btns {
+ position: absolute;
+ bottom: $base * 3;
+ }
+
+ .modal__tasks-header {
+ text-overflow: ellipsis;
+ max-width: 84%;
+ text-align: initial;
+ overflow: hidden;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ line-height: 1.3em;
+ }
+
+ .modal__tasks-descr {
+ word-break: break-word;
+ max-width: 96%;
+ }
+}
+
+.modal__text-question {
+ display: flex;
+}
diff --git a/src/components/modal/Modal.tsx b/src/components/modal/Modal.tsx
new file mode 100644
index 0000000..8e5b71e
--- /dev/null
+++ b/src/components/modal/Modal.tsx
@@ -0,0 +1,39 @@
+import React from 'react';
+import './Modal.scss';
+
+type IProps = {
+ activeModal: boolean;
+ setActiveModal: (active: boolean) => void;
+ children: JSX.Element | JSX.Element[];
+};
+
+const Modal = (props: IProps) => {
+ //for control pls use const [activeModal, setActiveModal] = useState(false);
+ return (
+ {
+ props.setActiveModal(false);
+ }}
+ >
+
e.stopPropagation()}
+ >
+ {props.children}
+
{
+ props.setActiveModal(false);
+ }}
+ >
+ 🞫
+
+
+
+ );
+};
+
+export default Modal;
diff --git a/src/components/modal/components/ErrorSign/ErrorSign.scss b/src/components/modal/components/ErrorSign/ErrorSign.scss
new file mode 100644
index 0000000..2836929
--- /dev/null
+++ b/src/components/modal/components/ErrorSign/ErrorSign.scss
@@ -0,0 +1,22 @@
+@import '../../../../sass/Constants.scss';
+
+.error-modal {
+ @include flexCenter;
+ flex-direction: column;
+ gap: $base * 6;
+ width: 100%;
+ height: 100%;
+
+ .error-modal__title {
+ color: $color-error;
+ font-size: $fs-xxxl;
+ }
+
+ .error-modal__title.green {
+ color: $color-green;
+ }
+}
+
+.error-content {
+ margin: auto;
+}
diff --git a/src/components/modal/components/ErrorSign/ErrorSign.tsx b/src/components/modal/components/ErrorSign/ErrorSign.tsx
new file mode 100644
index 0000000..51df29c
--- /dev/null
+++ b/src/components/modal/components/ErrorSign/ErrorSign.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import { useAppSelector } from '../../../../app/hooks';
+import { localizationObj } from '../../../../features/localization';
+import './ErrorSign.scss';
+
+type Error = {
+ errorMsg: string;
+};
+
+const ErrorSign = ({ errorMsg }: Error) => {
+ const { lang } = useAppSelector((state) => state.langStorage);
+ return (
+
+
{localizationObj[lang].error}
+
{errorMsg}
+
+ );
+};
+
+export default ErrorSign;
diff --git a/src/components/modal/components/index.ts b/src/components/modal/components/index.ts
new file mode 100644
index 0000000..72cdd5f
--- /dev/null
+++ b/src/components/modal/components/index.ts
@@ -0,0 +1,3 @@
+import ErrorSign from './ErrorSign/ErrorSign';
+
+export { ErrorSign };
diff --git a/src/components/preloader/Preloader.scss b/src/components/preloader/Preloader.scss
new file mode 100644
index 0000000..5c37f0f
--- /dev/null
+++ b/src/components/preloader/Preloader.scss
@@ -0,0 +1,13 @@
+@import '../../sass/Constants.scss';
+
+.preloader {
+ width: $base * 25;
+ height: $base * 25;
+ margin: auto;
+ background-color: transparent;
+ background-image: url('../../images/icons/loading.gif');
+ background-size: contain;
+ background-position: center;
+ background-repeat: no-repeat;
+ border-radius: 50%;
+}
diff --git a/src/components/preloader/Preloader.tsx b/src/components/preloader/Preloader.tsx
new file mode 100644
index 0000000..6c7bd10
--- /dev/null
+++ b/src/components/preloader/Preloader.tsx
@@ -0,0 +1,9 @@
+import { FC } from 'react';
+
+import './Preloader.scss';
+
+const Preloader: FC = () => {
+ return ;
+};
+
+export { Preloader };
diff --git a/src/components/preloader/index.tsx b/src/components/preloader/index.tsx
new file mode 100644
index 0000000..c42544a
--- /dev/null
+++ b/src/components/preloader/index.tsx
@@ -0,0 +1,12 @@
+import { ReactElement, Suspense } from 'react';
+import { Preloader } from './Preloader';
+
+type Children = {
+ children: ReactElement;
+};
+
+const PreloaderSuspense = ({ children }: Children) => {
+ return }>{children};
+};
+
+export { PreloaderSuspense };
diff --git a/src/components/requireAuth/RequireAuth.tsx b/src/components/requireAuth/RequireAuth.tsx
new file mode 100644
index 0000000..3f031e1
--- /dev/null
+++ b/src/components/requireAuth/RequireAuth.tsx
@@ -0,0 +1,23 @@
+import React from 'react';
+import { useLocation, Navigate } from 'react-router-dom';
+import { useAppSelector } from '../../app/hooks';
+import { PATHS } from '../../shared/constants/routes';
+
+const RequireAuth = ({ children, needAuth }: { children: JSX.Element; needAuth: boolean }) => {
+ const { userToken } = useAppSelector((state) => state.authStorage);
+ const location = useLocation();
+
+ const permission = needAuth ? !!userToken : !userToken;
+
+ if (!permission && needAuth) {
+ return ;
+ }
+
+ if (!permission && !needAuth) {
+ return ;
+ }
+
+ return children;
+};
+
+export default RequireAuth;
diff --git a/src/components/textarea/Textarea.scss b/src/components/textarea/Textarea.scss
new file mode 100644
index 0000000..999faee
--- /dev/null
+++ b/src/components/textarea/Textarea.scss
@@ -0,0 +1,26 @@
+@import '../../sass/Constants.scss';
+
+.textarea {
+ @include poppins;
+ width: 100%;
+ padding: $base $base * 2;
+ color: $color-dark-09;
+ background-color: $color-grey-07;
+ border-width: 0.063rem;
+ border-style: solid;
+ border-color: $color-dark-09;
+ border-radius: $base;
+ resize: none;
+ transition: $transition;
+
+ &::placeholder {
+ color: $color-dark-08;
+ }
+
+ &:hover,
+ &:focus,
+ &:active {
+ border-color: $color-dark;
+ transition: $transition;
+ }
+}
diff --git a/src/components/textarea/Textarea.tsx b/src/components/textarea/Textarea.tsx
new file mode 100644
index 0000000..a944cba
--- /dev/null
+++ b/src/components/textarea/Textarea.tsx
@@ -0,0 +1,37 @@
+import { FC, ChangeEvent } from 'react';
+
+import './Textarea.scss';
+
+interface TextareaProps {
+ className: string;
+ cols: number;
+ rows: number;
+ placeholder: string;
+ value: string;
+ onChange: (event: ChangeEvent) => void;
+ onClick?: (event: React.MouseEvent) => void;
+}
+
+const Textarea: FC = ({
+ className,
+ cols,
+ rows,
+ placeholder,
+ value,
+ onChange,
+ onClick,
+}) => {
+ return (
+
+ );
+};
+
+export { Textarea };
diff --git a/src/features/decodeUserId.ts b/src/features/decodeUserId.ts
new file mode 100644
index 0000000..1f453ea
--- /dev/null
+++ b/src/features/decodeUserId.ts
@@ -0,0 +1,13 @@
+import jwt_decode from 'jwt-decode';
+
+const decodeUserId = (userToken: string) => {
+ try {
+ if (userToken) {
+ const decoded: { userId: string } = jwt_decode(userToken);
+ return decoded.userId;
+ }
+ } catch (e) {}
+ return '';
+};
+
+export default decodeUserId;
diff --git a/src/features/localization.ts b/src/features/localization.ts
new file mode 100644
index 0000000..21f52e1
--- /dev/null
+++ b/src/features/localization.ts
@@ -0,0 +1,127 @@
+import { enLang, ruLang } from './ls-load-save';
+
+export const localizationObj = {
+ [ruLang]: {
+ signIn: 'Вход',
+ signUp: 'Регистрация',
+ signOut: 'Выход',
+ language: 'Язык',
+ profile: 'Профиль',
+ meetOurTeam: 'Наша команда',
+ login: 'Логин',
+ password: 'Пароль',
+ name: 'Имя',
+ repeatPassword: 'Повторите пароль',
+ editUser: 'Редактировать пользователя',
+ changePassword: 'Сменить пароль',
+ deleteUser: 'Удалить пользователя',
+ oldPassword: 'Старый пароль',
+ newPassword: 'Новый пароль',
+ enterYourPassword: 'Введите свой пароль',
+ enterYourLogin: 'Введите свой логин',
+ enterYourName: 'Введите свое имя',
+ emptyField: 'Пустое поле',
+ onlyNumbersLetters: 'Только цифры и латинские буквы',
+ lessThanFour: ' не может быть короче 4 символов',
+ passwordsMatch: 'Пароли не совпадают',
+ createBoard: 'Создать новую доску',
+ createColumn: 'Добавить колонку',
+ createTask: 'Добавить задачу',
+ yourBoards: 'Ваши доски',
+ board: 'Доска',
+ boards: 'Доски',
+ boardsHeader: 'На главную',
+ openBoard: 'Открыть доску',
+ back: 'Назад',
+ submit: 'Подтвердить',
+ cancel: 'Отмена',
+ doYouWantToDelete: 'Вы хотите удалить',
+ areYouSure: 'Вы уверены?',
+ error: 'Ошибка!',
+ success: 'Успех!',
+ updateSuccessful: 'обновлен успешно!',
+ user: 'Пользователь',
+ wrongPassword: 'Неверный пароль',
+ afterDeleteRedirect:
+ 'После удаления пользователя вы будете перенаправлены на главную страницу!',
+ update: 'Обновить',
+ loading: 'Загрузка...',
+ addATitle: 'Добавить заголовок',
+ addADescription: 'Добавить описание',
+ author: 'Пользователь',
+ yourTask: 'Задача',
+ taskDescription: 'Описание',
+ shatau: 'Евгений Шатов',
+ kochieva: 'Виктория Кочиева',
+ karakulka: 'Дмитрий Каракулько',
+ doYouWantToChangeUser: 'Выберите нового пользователя',
+ welcome: 'Добро пожаловать',
+ app: 'Ваша система управлениями проектами',
+ aboutProject:
+ 'Приложение для управления проектами — это приложение, которое назначает задачи отдельным участникам проекта или группы. Это визуальный инструмент, который позволяет вашей команде управлять любым типом проекта, рабочим процессом или отслеживанием задач. Добавьте файлы, контрольные списки или даже автоматизацию: настройте все так, чтобы ваша команда работала лучше всего. Просто зарегистрируйтесь, создайте доску, и вперед!',
+ course:
+ 'RS School — это бесплатная образовательная программа, проводимая сообществом разработчиков The Rolling Scopes с 2013 года. Учиться в RS School может каждый, независимо от возраста, профессиональной занятости и места жительства. Наставники и тренеры нашей школы — фронтенд- и javascript-разработчики из разных компаний и стран. В RS School работает принцип «Плати вперед». Мы бесплатно делимся с учащимися своими знаниями в настоящее время, надеясь, что в будущем они вернутся к нам в качестве наставников и точно так же передадут свои знания последующему поколению студентов.',
+ courseAbout: 'О курсе',
+ },
+ [enLang]: {
+ signIn: 'Sign In',
+ signUp: 'Sign Up',
+ signOut: 'Sign Out',
+ language: 'Language',
+ profile: 'Profile',
+ meetOurTeam: 'Meet our team',
+ login: 'Login',
+ password: 'Password',
+ name: 'Name',
+ repeatPassword: 'Repeat password',
+ editUser: 'Edit user',
+ changePassword: 'Change password',
+ deleteUser: 'Delete user',
+ oldPassword: 'Old password',
+ newPassword: 'New password',
+ enterYourPassword: 'Enter your password',
+ enterYourLogin: 'Enter your login',
+ enterYourName: 'Enter your name',
+ emptyField: 'Empty field!',
+ onlyNumbersLetters: 'Only numbers and english letters',
+ lessThanFour: ' can not be less than 4 symbols',
+ passwordsMatch: 'Passwords do not match',
+ createBoard: 'Create a new board',
+ createColumn: 'New column',
+ createTask: 'Add task',
+ yourBoards: 'Your boards',
+ board: 'Board',
+ boards: 'Boards',
+ boardsHeader: 'Main page',
+ openBoard: 'Open board',
+ back: 'Back',
+ submit: 'Submit',
+ cancel: 'Cancel',
+ doYouWantToDelete: 'Do you want to delete ',
+ areYouSure: 'Are you sure?',
+ error: 'Error!',
+ success: 'Success!',
+ updateSuccessful: 'update successful!',
+ user: 'User',
+ wrongPassword: 'Wrong password',
+ afterDeleteRedirect: 'After deletion, you will be redirected to the welcome page!',
+ update: 'Update',
+ loading: 'Loading...',
+ addATitle: 'Add a title',
+ addADescription: 'Add a description',
+ author: 'User',
+ yourTask: 'Task',
+ taskDescription: 'Description',
+ shatau: 'Yauheni Shatau',
+ kochieva: 'Victoria Kochieva',
+ karakulka: 'Dzmitry Karakulka',
+ doYouWantToChangeUser: 'Choose new task user',
+ welcome: 'Welcome',
+ app: "It's Your Project Management App",
+ aboutProject:
+ "Project management app is an application that assigns tasks to individual participants in a project or group. It is the visual tool that empowers your team to manage any type of project, workflow, or task tracking. Add files, checklists, or even automation: Customize it all for how your team works best. Just sign up, create a board, and you're off!",
+ course:
+ 'RS School is free-of-charge and community-based education program conducted by The Rolling Scopes developer community since 2013. Everyone can study at RS School, regardless of age, professional employment, or place of residence. The mentors and trainers of our school are front-end and javascript developers from different companies and countries. RS School operates "Pay it forward" principle. We share our knowledge with students for free at the present time, hoping that in the future they will return to us as mentors and pass on their knowledge to the next generation of students in the same way.',
+ courseAbout: 'About the course',
+ },
+};
diff --git a/src/features/ls-load-save.ts b/src/features/ls-load-save.ts
new file mode 100644
index 0000000..cd527ba
--- /dev/null
+++ b/src/features/ls-load-save.ts
@@ -0,0 +1,25 @@
+import { LangRuEn } from '../reducers/langReducer';
+
+const team53Auth = 'team53-auth';
+const team53Lang = 'team53-lang';
+export const ruLang = 'RU';
+export const enLang = 'EN';
+
+export const loadTokenFromLS = (): string => {
+ if (!localStorage.getItem(team53Auth)) localStorage.setItem(team53Auth, '');
+ return localStorage.getItem(team53Auth) as string;
+};
+
+export const saveTokenToLS = (token: string) => {
+ localStorage.setItem(team53Auth, token);
+};
+
+export const loadLangFromLS = (): LangRuEn => {
+ if (!localStorage.getItem(team53Lang)) localStorage.setItem(team53Lang, enLang);
+ const lang = localStorage.getItem(team53Lang);
+ return lang ? (lang as LangRuEn) : enLang;
+};
+
+export const saveLangToLS = (lang: LangRuEn) => {
+ localStorage.setItem(team53Lang, lang);
+};
diff --git a/src/fonts/Poppins-Bold.woff b/src/fonts/Poppins-Bold.woff
new file mode 100644
index 0000000..94bdc27
Binary files /dev/null and b/src/fonts/Poppins-Bold.woff differ
diff --git a/src/fonts/Poppins-Bold.woff2 b/src/fonts/Poppins-Bold.woff2
new file mode 100644
index 0000000..5f5008f
Binary files /dev/null and b/src/fonts/Poppins-Bold.woff2 differ
diff --git a/src/fonts/Poppins-Medium.woff b/src/fonts/Poppins-Medium.woff
new file mode 100644
index 0000000..3fbd4a2
Binary files /dev/null and b/src/fonts/Poppins-Medium.woff differ
diff --git a/src/fonts/Poppins-Medium.woff2 b/src/fonts/Poppins-Medium.woff2
new file mode 100644
index 0000000..ceecc77
Binary files /dev/null and b/src/fonts/Poppins-Medium.woff2 differ
diff --git a/src/fonts/Poppins-Regular.woff b/src/fonts/Poppins-Regular.woff
new file mode 100644
index 0000000..01d94b4
Binary files /dev/null and b/src/fonts/Poppins-Regular.woff differ
diff --git a/src/fonts/Poppins-Regular.woff2 b/src/fonts/Poppins-Regular.woff2
new file mode 100644
index 0000000..4aae28c
Binary files /dev/null and b/src/fonts/Poppins-Regular.woff2 differ
diff --git a/src/fonts/Poppins-SemiBold.woff b/src/fonts/Poppins-SemiBold.woff
new file mode 100644
index 0000000..208e285
Binary files /dev/null and b/src/fonts/Poppins-SemiBold.woff differ
diff --git a/src/fonts/Poppins-SemiBold.woff2 b/src/fonts/Poppins-SemiBold.woff2
new file mode 100644
index 0000000..990f6f7
Binary files /dev/null and b/src/fonts/Poppins-SemiBold.woff2 differ
diff --git a/src/images/YFNLt5hMncNXRFTyJMBGis_ri01GarCduTNi4PLDKgcMxFbphXYXLyaWJDNkFmu.jpg b/src/images/YFNLt5hMncNXRFTyJMBGis_ri01GarCduTNi4PLDKgcMxFbphXYXLyaWJDNkFmu.jpg
new file mode 100644
index 0000000..e2fa9e1
Binary files /dev/null and b/src/images/YFNLt5hMncNXRFTyJMBGis_ri01GarCduTNi4PLDKgcMxFbphXYXLyaWJDNkFmu.jpg differ
diff --git a/src/images/avatar-1.jpg b/src/images/avatar-1.jpg
new file mode 100644
index 0000000..6a4d021
Binary files /dev/null and b/src/images/avatar-1.jpg differ
diff --git a/src/images/avatar-2.jpg b/src/images/avatar-2.jpg
new file mode 100644
index 0000000..19012a4
Binary files /dev/null and b/src/images/avatar-2.jpg differ
diff --git a/src/images/avatar-3.jpg b/src/images/avatar-3.jpg
new file mode 100644
index 0000000..836e909
Binary files /dev/null and b/src/images/avatar-3.jpg differ
diff --git a/src/images/confirmation.jpg b/src/images/confirmation.jpg
new file mode 100644
index 0000000..c3295d5
Binary files /dev/null and b/src/images/confirmation.jpg differ
diff --git a/src/images/icons/back.svg b/src/images/icons/back.svg
new file mode 100644
index 0000000..0055b09
--- /dev/null
+++ b/src/images/icons/back.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/images/icons/boards.svg b/src/images/icons/boards.svg
new file mode 100644
index 0000000..71567b4
--- /dev/null
+++ b/src/images/icons/boards.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/images/icons/cancel.svg b/src/images/icons/cancel.svg
new file mode 100644
index 0000000..2f79e02
--- /dev/null
+++ b/src/images/icons/cancel.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/images/icons/delete.svg b/src/images/icons/delete.svg
new file mode 100644
index 0000000..c442f7f
--- /dev/null
+++ b/src/images/icons/delete.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/images/icons/edit.svg b/src/images/icons/edit.svg
new file mode 100644
index 0000000..9a26ee6
--- /dev/null
+++ b/src/images/icons/edit.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/images/icons/github.svg b/src/images/icons/github.svg
new file mode 100644
index 0000000..22cef09
--- /dev/null
+++ b/src/images/icons/github.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/images/icons/instagram.svg b/src/images/icons/instagram.svg
new file mode 100644
index 0000000..6f438be
--- /dev/null
+++ b/src/images/icons/instagram.svg
@@ -0,0 +1,14 @@
+
+
+
diff --git a/src/images/icons/linkedin.svg b/src/images/icons/linkedin.svg
new file mode 100644
index 0000000..47bd395
--- /dev/null
+++ b/src/images/icons/linkedin.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/images/icons/loading.gif b/src/images/icons/loading.gif
new file mode 100644
index 0000000..b7a1b23
Binary files /dev/null and b/src/images/icons/loading.gif differ
diff --git a/src/images/icons/logo-small.png b/src/images/icons/logo-small.png
new file mode 100644
index 0000000..80e178a
Binary files /dev/null and b/src/images/icons/logo-small.png differ
diff --git a/src/images/icons/logo.svg b/src/images/icons/logo.svg
new file mode 100644
index 0000000..a6a5210
--- /dev/null
+++ b/src/images/icons/logo.svg
@@ -0,0 +1,10 @@
+
diff --git a/src/images/icons/plus.svg b/src/images/icons/plus.svg
new file mode 100644
index 0000000..8a4da87
--- /dev/null
+++ b/src/images/icons/plus.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/images/icons/rs_school_logo.svg b/src/images/icons/rs_school_logo.svg
new file mode 100644
index 0000000..3b80540
--- /dev/null
+++ b/src/images/icons/rs_school_logo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/images/icons/submit.svg b/src/images/icons/submit.svg
new file mode 100644
index 0000000..0e4e668
--- /dev/null
+++ b/src/images/icons/submit.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/images/icons/user.svg b/src/images/icons/user.svg
new file mode 100644
index 0000000..bd51809
--- /dev/null
+++ b/src/images/icons/user.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/images/modal/changeUser.jpg b/src/images/modal/changeUser.jpg
new file mode 100644
index 0000000..e2d3e38
Binary files /dev/null and b/src/images/modal/changeUser.jpg differ
diff --git a/src/images/modal/create.jpg b/src/images/modal/create.jpg
new file mode 100644
index 0000000..1e0b3a1
Binary files /dev/null and b/src/images/modal/create.jpg differ
diff --git a/src/images/modal/defaultBg.jpg b/src/images/modal/defaultBg.jpg
new file mode 100644
index 0000000..d1ca245
Binary files /dev/null and b/src/images/modal/defaultBg.jpg differ
diff --git a/src/index.scss b/src/index.scss
new file mode 100644
index 0000000..a7c9573
--- /dev/null
+++ b/src/index.scss
@@ -0,0 +1,107 @@
+@import './sass/Constants.scss';
+
+// === FONTS ===
+
+@mixin font($font_name, $file_name, $weight, $style) {
+ @font-face {
+ font-family: $font_name;
+ font-display: swap;
+ src: url('./fonts/#{$file_name}.woff') format('woff'),
+ url('./fonts/#{$file_name}.woff2') format('woff2');
+ font-weight: #{$weight};
+ font-style: #{$style};
+ }
+}
+
+@import './sass/Fonts.scss';
+
+// === / FONTS ===
+
+html,
+body {
+ @include poppins;
+ position: relative;
+ width: 100%;
+ height: 100%;
+ overflow-x: hidden;
+ scroll-behavior: smooth;
+}
+
+body {
+ @include scroll;
+ overflow: auto;
+}
+
+* {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+ list-style-type: none;
+}
+
+.main {
+ flex-grow: 1;
+}
+
+#root {
+ display: flex;
+ justify-content: space-between;
+ flex-direction: column;
+ height: 100vh;
+}
+
+.wrapper {
+ display: flex;
+ width: 80%;
+ max-width: $base * 288;
+ margin: 0 auto;
+
+ @media (max-width: $breakpoint-lg-min) {
+ width: 97%;
+ }
+}
+
+.h2 {
+ @include poppins;
+ margin: 0;
+ padding: 0;
+ font-size: $fs-xxl;
+ font-weight: $fw-semi-bold;
+ letter-spacing: 0.125rem;
+ text-align: center;
+ text-transform: uppercase;
+}
+
+.h3 {
+ @include poppins;
+ margin: 0;
+ padding: 0;
+ font-size: $fs-xs;
+ font-weight: $fw-semi-bold;
+ color: $color-white;
+ text-align: center;
+ text-transform: uppercase;
+}
+
+.h4 {
+ @include poppins;
+ margin: 0;
+ padding: 0;
+ font-size: $fs-m;
+ font-weight: $fw-bold;
+ color: $color-dark;
+ line-height: $base * 4;
+ letter-spacing: 0.06em;
+}
+
+a {
+ text-decoration: none;
+}
+
+.border-right {
+ display: flex;
+ align-items: center;
+ height: 100%;
+ padding-right: $base;
+ border-right: 0.063rem solid $color-dark-03;
+}
diff --git a/src/index.tsx b/src/index.tsx
new file mode 100644
index 0000000..fef7dfa
--- /dev/null
+++ b/src/index.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import { Provider } from 'react-redux';
+import { BrowserRouter } from 'react-router-dom';
+import { store } from './app/store';
+
+import { App } from './App';
+import './index.scss';
+
+// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+const container = document.getElementById('root')!;
+const root = createRoot(container);
+
+root.render(
+
+
+
+
+
+);
diff --git a/src/middleware/ls-middleware.ts b/src/middleware/ls-middleware.ts
new file mode 100644
index 0000000..3fa2113
--- /dev/null
+++ b/src/middleware/ls-middleware.ts
@@ -0,0 +1,13 @@
+import { Action, AnyAction, Dispatch } from '@reduxjs/toolkit';
+import { saveLangToLS } from '../features/ls-load-save';
+import { setLang } from '../reducers/langReducer';
+
+export const localStorageMiddleware =
+ () => (next: Dispatch) => (action: Action) => {
+ if (setLang.match(action)) {
+ try {
+ saveLangToLS(action.payload);
+ } catch {}
+ }
+ return next(action);
+ };
diff --git a/src/pages/board/Board.scss b/src/pages/board/Board.scss
new file mode 100644
index 0000000..2f6dc7a
--- /dev/null
+++ b/src/pages/board/Board.scss
@@ -0,0 +1,95 @@
+@import '../../sass/Constants.scss';
+
+.board {
+ display: flex;
+ justify-content: space-between;
+ flex-direction: column;
+ height: 100%;
+ max-height: max-content;
+ background-color: $color-purple-06;
+
+ .wrapper {
+ align-items: flex-start;
+ gap: $base;
+ height: 100%;
+ }
+}
+
+.board__wrapper {
+ column-gap: $base * 2;
+ height: inherit;
+ overflow-x: auto;
+ overflow-y: hidden;
+}
+
+.board__columns {
+ display: flex;
+ align-items: flex-start;
+ column-gap: $base * 3;
+ height: 100%;
+}
+
+.board__column::-webkit-scrollbar,
+.board__wrapper::-webkit-scrollbar {
+ width: $base;
+}
+
+.board__column::-webkit-scrollbar-thumb,
+.board__wrapper::-webkit-scrollbar-thumb {
+ background-color: $color-dark-02;
+ border-radius: $base;
+}
+
+.board__column::-webkit-scrollbar-track,
+.board__wrapper::-webkit-scrollbar-track {
+ box-shadow: inset 0 0 $base grey;
+ border-radius: $base * 2;
+ height: $base * 2;
+ background-color: $color-grey-white;
+}
+
+.column__new-btn {
+ height: fit-content;
+}
+
+.board__title__wrapper {
+ display: flex;
+ align-items: flex-start;
+ flex-direction: column;
+ row-gap: $base * 2;
+ width: max-content;
+ text-align: center;
+
+ @media (max-width: $breakpoint-lg-min) {
+ align-items: center;
+ }
+}
+
+.board__title {
+ width: 80%;
+ max-width: 90rem;
+ margin: 0 auto;
+ font-size: $fs-xxxl;
+ font-weight: $fw-semi-bold;
+ text-align: left;
+ line-height: inherit;
+ word-break: break-word;
+ text-shadow: 0.063rem 0.063rem 0.063rem $color-sale, 0 0 0.06em $color-blue-light,
+ 0 0 0.06em $color-blue-light;
+ color: $color-dark;
+
+ @media (max-width: $breakpoint-lg-min) {
+ width: 97%;
+ font-size: $fs-xxl;
+ }
+
+ @media (max-width: $breakpoint-md-min) {
+ font-size: $fs-xl;
+ }
+}
+
+.board__title-description {
+ @media (max-width: $breakpoint-lg-min) {
+ display: none;
+ }
+}
diff --git a/src/pages/board/Board.tsx b/src/pages/board/Board.tsx
new file mode 100644
index 0000000..f716ca1
--- /dev/null
+++ b/src/pages/board/Board.tsx
@@ -0,0 +1,301 @@
+import React, { FC, useEffect, useRef, useState } from 'react';
+import { DragDropContext, Droppable, DropResult } from 'react-beautiful-dnd';
+import { Link, useParams } from 'react-router-dom';
+
+import {
+ apiUser,
+ useDeleteTaskMutation,
+ useGetBoardsByIdQuery,
+ useGetColumnsQuery,
+ usePostColumnMutation,
+ usePostTaskMutation,
+ useUpdateColumnMutation,
+ useUpdateTaskMutation,
+} from '../../app/RtkQuery';
+import { BackButton } from '../../components/buttons';
+import { localizationObj } from '../../features/localization';
+import { Preloader } from '../../components/preloader/Preloader';
+
+import './Board.scss';
+import { Modal } from '../../components';
+import { useAppDispatch, useAppSelector } from '../../app/hooks';
+import { TasksList } from '../../components/boardColumn/BoardColumn';
+import { ChangeTitleBtns } from '../../components/buttons';
+const BoardColumn = React.lazy(() => import('../../components/boardColumn/BoardColumn'));
+
+export interface ColumnType {
+ title: string;
+ id: string;
+ order: number;
+}
+
+const Board: FC = () => {
+ const { id } = useParams();
+ const boardId = id ?? '';
+
+ const api = apiUser;
+ const dispatch = useAppDispatch();
+ const { userId } = useAppSelector((state) => state.userStorage);
+
+ const { data: data1, error, isLoading } = useGetColumnsQuery({ boardId });
+ const [postColumn] = usePostColumnMutation();
+ const [updateColumn] = useUpdateColumnMutation();
+ const [postTask] = usePostTaskMutation();
+ const [deleteTask] = useDeleteTaskMutation();
+ const [updateTask] = useUpdateTaskMutation();
+ const getBoardsById = useGetBoardsByIdQuery(boardId);
+ const currentBoardTitle = getBoardsById.data?.title;
+ const moveRef = useRef(['', '']);
+ const { lang } = useAppSelector((state) => state.langStorage);
+ const [columnsList, updateColumnsList] = useState([]);
+ const [activeModal, setActiveModal] = useState(false);
+ const [columnTitle, setColumnTitle] = useState('');
+
+ const addNewColumn = async () => {
+ if (columnTitle.trim().length) {
+ await postColumn({
+ boardId: boardId,
+ body: {
+ title: columnTitle,
+ },
+ });
+ setActiveModal(false);
+ setColumnTitle('');
+ }
+ };
+
+ const updateColumnHandler = async (id: string, title: string, order: number) => {
+ await updateColumn({
+ columnId: id,
+ boardId,
+ body: {
+ title,
+ order,
+ },
+ });
+ };
+
+ const reorderColumns = (columnsList: ColumnType[], startIndex: number, endIndex: number) => {
+ const columns = [...columnsList];
+ const [movedColumn] = columns.splice(startIndex, 1);
+ columns.splice(endIndex, 0, movedColumn);
+ updateColumnHandler(movedColumn.id, movedColumn.title, endIndex + 1);
+ return columns;
+ };
+
+ const addTaskToAnotherColumn = async (
+ columnId: string,
+ taskTitle: string,
+ description: string,
+ userId: string
+ ) => {
+ return await postTask({
+ columnId,
+ boardId,
+ body: {
+ title: taskTitle,
+ description: description,
+ userId,
+ },
+ });
+ };
+
+ const deleteTaskFromCurrentCol = async (columnId: string, taskId: string) => {
+ return await deleteTask({
+ columnId,
+ boardId,
+ taskId,
+ });
+ };
+
+ const updateTaskHandler = async (
+ columnId: string,
+ taskTitle: string,
+ taskId: string,
+ order: number,
+ description: string,
+ userId: string
+ ) => {
+ return await updateTask({
+ columnId: columnId,
+ boardId,
+ taskId,
+ task: {
+ title: taskTitle,
+ order,
+ description: description,
+ userId,
+ },
+ });
+ };
+
+ const reorderTasks = async (
+ tasksList: TasksList[],
+ endIndex: number,
+ columnId: string,
+ taskId: string
+ ) => {
+ const [movedTask] = tasksList.filter((task) => task.id === taskId);
+ await updateTaskHandler(
+ columnId,
+ movedTask.title,
+ taskId,
+ endIndex + 1,
+ movedTask.description,
+ movedTask.userId
+ );
+ };
+
+ const onDragEndHandler = (result: DropResult) => {
+ const { source, destination, draggableId, type } = result;
+
+ if (!destination) return;
+
+ if (destination.droppableId === boardId && type === 'tasks') return;
+
+ //if dragging to another column
+ if (
+ source.droppableId !== destination.droppableId &&
+ type === 'tasks' &&
+ !moveRef.current.includes(draggableId)
+ ) {
+ moveRef.current.push(draggableId);
+ moveRef.current.shift();
+
+ const task = dispatch(
+ api.endpoints.getTaskById.initiate({
+ columnId: source.droppableId,
+ boardId,
+ taskId: draggableId,
+ })
+ );
+
+ task
+ .then((res) =>
+ Promise.all([
+ addTaskToAnotherColumn(
+ destination.droppableId,
+ res.data.title,
+ res.data.description,
+ res.data.userId
+ ),
+ deleteTaskFromCurrentCol(source.droppableId, draggableId),
+ ])
+ )
+ .catch(() => {});
+ task.unsubscribe();
+ return;
+ }
+
+ //if dragging inside column
+ if (source.droppableId === destination.droppableId && type === 'tasks') {
+ const tasks = dispatch(
+ api.endpoints.getTasks.initiate({
+ columnId: source.droppableId,
+ boardId,
+ })
+ );
+
+ tasks
+ .then((res) => res.data)
+ .then((data) => reorderTasks(data, destination.index, source.droppableId, draggableId))
+ .catch(() => {});
+ tasks.unsubscribe();
+ return;
+ }
+
+ //dragColumns
+ if (type === 'columns') {
+ const columns: ColumnType[] = reorderColumns(columnsList, source.index, destination.index);
+ updateColumnsList(columns);
+ }
+ };
+
+ useEffect(() => {
+ if (data1) {
+ updateColumnsList([...data1].sort((a: ColumnType, b: ColumnType) => a.order - b.order));
+ }
+ }, [data1]);
+
+ return (
+ <>
+
+
+
+ {localizationObj[lang].board}
+ {currentBoardTitle}
+
+
+
+
+
+
+
+ setActiveModal(true)}
+ />
+
+ {!isLoading ? (
+
+ {(provided) => (
+
+ {columnsList.map(({ title, id, order }: ColumnType, index: number) => {
+ return (
+
+ );
+ })}
+ {provided.placeholder}
+
+ )}
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+
+
{`${localizationObj[lang].addATitle}`}
+ ) =>
+ setColumnTitle(event?.target.value)
+ }
+ />
+ setActiveModal(false)}
+ />
+
+
+
+ >
+ );
+};
+
+export { Board };
diff --git a/src/pages/boards/Boards.scss b/src/pages/boards/Boards.scss
new file mode 100644
index 0000000..364d5c2
--- /dev/null
+++ b/src/pages/boards/Boards.scss
@@ -0,0 +1,124 @@
+@import '../../sass/Constants.scss';
+
+.boards {
+ padding: 0 0 $base * 5 0;
+ width: 80%;
+ margin: 0 auto;
+
+ @media (max-width: $breakpoint-lg-min) {
+ width: 97%;
+ }
+
+ .h2 {
+ @include disableSelection;
+ margin-top: $base * 2;
+ font-size: $fs-xxxxl;
+ text-shadow: 0.063rem 0.063rem 0.063rem $color-purple, 0 0 0.06em $color-purple,
+ 0 0 0.06em $color-purple;
+
+ @media (max-width: $breakpoint-lg-min) {
+ font-size: $fs-xxxl;
+ }
+
+ @media (max-width: $breakpoint-md-min) {
+ font-size: $fs-xxl;
+ }
+ }
+}
+
+.boards__container {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ column-gap: $base * 2;
+ row-gap: $base * 4;
+ padding: $base * 5 0;
+}
+
+.board__new-btn {
+ flex-basis: 30%;
+ height: $base * 10;
+}
+
+.boards__item-title {
+ font-size: $fs-xl;
+ font-weight: $fw-bold;
+ color: $color-dark;
+ transition: $transition-02;
+ cursor: pointer;
+}
+
+.boards__item-descr {
+ font-size: $fs-m;
+ font-weight: $fw-semi-bold;
+ color: $color-purple;
+ cursor: pointer;
+ transition: $transition-01;
+
+ &:hover {
+ color: $color-blue-09;
+ transition: $transition-01;
+ }
+}
+
+.boards__item-input__wrapper {
+ display: flex;
+ flex-direction: column;
+ row-gap: $base;
+ margin-bottom: $base;
+
+ .textarea {
+ @include poppins;
+ width: 100%;
+ padding: 0 $base * 2;
+ font-size: $fs-l;
+ color: $color-dark;
+ background-color: $color-white;
+ border: 0.063rem solid $color-dark;
+ }
+}
+
+.boards__item-input {
+ @include poppins;
+ width: 100%;
+ padding: 0 $base * 2;
+ font-size: $fs-l;
+ color: $color-dark;
+ background-color: $color-white;
+ border: 0.063rem solid $color-dark;
+ border-radius: $base;
+
+ @media (max-width: $breakpoint-md-min) {
+ font-size: $fs-m;
+ }
+}
+
+.boards-item__link {
+ display: flex;
+ justify-content: space-between;
+ flex-direction: column;
+ flex-basis: 32.4%;
+ height: 100%;
+ min-height: $base * 35;
+ padding: $base * 2;
+ word-break: break-word;
+ border: 0.063rem solid $color-dark-03;
+ box-shadow: 0.063rem 0.063rem 0.063rem $color-dark-05, 0 0 0.06em $color-dark,
+ 0 0 0.06em $color-dark;
+ border-radius: $base * 3;
+
+ &:hover {
+ .boards__item-title {
+ color: $color-yellow;
+ transition: $transition-02;
+ }
+ }
+
+ @media (max-width: $breakpoint-md-min) {
+ flex-basis: 48%;
+ }
+
+ @media (max-width: $breakpoint-xs-min) {
+ flex-basis: 100%;
+ }
+}
diff --git a/src/pages/boards/Boards.tsx b/src/pages/boards/Boards.tsx
new file mode 100644
index 0000000..9b3577e
--- /dev/null
+++ b/src/pages/boards/Boards.tsx
@@ -0,0 +1,92 @@
+import React, { FC, useState, MouseEvent, lazy } from 'react';
+import { Link } from 'react-router-dom';
+import {
+ useDeleteBoardMutation,
+ useGetBoardsQuery,
+ useGetUsersQuery,
+ usePostBoardMutation,
+} from '../../app/RtkQuery';
+
+import { Modal, PreloaderSuspense } from '../../components';
+import { Preloader } from '../../components/preloader/Preloader';
+
+import { BoardsTypes } from './typesBoards/TypesBoards';
+
+import './Boards.scss';
+import { useAppSelector } from '../../app/hooks';
+import { localizationObj } from '../../features/localization';
+import { ChangeTitleBtns } from '../../components/buttons';
+
+const BoardsItem = lazy(() => import('./BoardsItem'));
+
+const Boards: FC = () => {
+ const [activeModal, setActiveModal] = useState(false);
+ const [deletedBoardTitle, setDeletedBoardTitle] = useState('');
+ const [deletedBoardId, setDeletedBoardId] = useState('');
+ const { lang } = useAppSelector((state) => state.langStorage);
+
+ const { data = [], error, isLoading } = useGetBoardsQuery('');
+ const { data: users = [] } = useGetUsersQuery('');
+ const [postBoard] = usePostBoardMutation();
+ const [deleteBoard] = useDeleteBoardMutation();
+
+ const handlerModal = (isActiveModal: boolean) => {
+ setActiveModal(isActiveModal);
+ };
+
+ const getDeletedBoard = (deletedBoardTitle: string, deletedBoardId: string) => {
+ setDeletedBoardTitle(deletedBoardTitle);
+ setDeletedBoardId(deletedBoardId);
+ };
+
+ const cancelDeleteBoard = (event: MouseEvent) => {
+ event.preventDefault();
+ setActiveModal(false);
+ };
+
+ const deleteBoardItem = (event: MouseEvent) => {
+ event.preventDefault();
+ setActiveModal(false);
+ deleteBoard(deletedBoardId);
+ };
+
+ return (
+
+ {localizationObj[lang].yourBoards}
+
+ {!isLoading ? (
+
+ {data?.map(({ id, title, description }: BoardsTypes) => {
+ return (
+
+ );
+ })}
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
{localizationObj[lang].areYouSure}
+ {`${localizationObj[lang].doYouWantToDelete} '${deletedBoardTitle}' ?`}
+
+
+
+
+
+ );
+};
+
+export { Boards };
diff --git a/src/pages/boards/BoardsItem.tsx b/src/pages/boards/BoardsItem.tsx
new file mode 100644
index 0000000..722ba01
--- /dev/null
+++ b/src/pages/boards/BoardsItem.tsx
@@ -0,0 +1,154 @@
+import React, { ChangeEvent, MouseEvent, useState } from 'react';
+import { Link } from 'react-router-dom';
+import { useDeleteBoardMutation, useUpdateBoardMutation } from '../../app/RtkQuery';
+import { BoardsTypes } from './typesBoards/TypesBoards';
+import { DeleteButton, ChangeTitleBtns } from '../../components/buttons';
+import { Textarea } from '../../components';
+import { localizationObj } from '../../features/localization';
+import { useAppSelector } from '../../app/hooks';
+
+const BoardsItem = ({
+ title,
+ id,
+ description,
+ isActiveModal,
+ getDeletedBoard,
+ activeModalProps,
+}: BoardsTypes) => {
+ const [activeModal, setActiveModal] = useState(false);
+ const [currentTitle, setCurrentTitle] = useState(title);
+ const [currentDescription, setCurrentDescription] = useState(description);
+ const [isOpenBoardTitle, setIsOpenBoardTitle] = useState(false);
+ const [isOpenBoardDescr, setIsOpenBoardDescr] = useState(false);
+ const { lang } = useAppSelector((state) => state.langStorage);
+
+ const [updateBoard] = useUpdateBoardMutation();
+
+ const handlerActiveModal = (event: MouseEvent) => {
+ event.preventDefault();
+ setActiveModal(true);
+ isActiveModal(true);
+ getDeletedBoard(title, id);
+ };
+
+ const handleBoardTitle = (event: ChangeEvent) => {
+ event.preventDefault();
+ const VALUE = event.target.value;
+ setCurrentTitle(VALUE);
+ };
+
+ const handleBoardDescription = (event: ChangeEvent) => {
+ const VALUE = event.target.value;
+ setCurrentDescription(VALUE);
+ };
+
+ const submitBoardTitle = async (event: MouseEvent) => {
+ event.preventDefault();
+
+ if (currentTitle.trim().length) {
+ setIsOpenBoardTitle(false);
+ setIsOpenBoardDescr(false);
+ setCurrentDescription(description);
+
+ await updateBoard({
+ boardId: id,
+ body: { title: currentTitle, description },
+ });
+ }
+ };
+
+ const submitBoardDescr = async (event: MouseEvent) => {
+ event.preventDefault();
+
+ if (currentDescription.trim().length) {
+ setIsOpenBoardTitle(false);
+ setIsOpenBoardDescr(false);
+ setCurrentTitle(title);
+
+ await updateBoard({
+ boardId: id,
+ body: { title, description: currentDescription },
+ });
+ }
+ };
+
+ const cancelBoardDescr = async (event: MouseEvent) => {
+ event.preventDefault();
+ setCurrentDescription(description);
+ setIsOpenBoardDescr(false);
+ };
+
+ const cancelBoardTitle = async (event: MouseEvent) => {
+ event.preventDefault();
+ setCurrentTitle(title);
+ setIsOpenBoardTitle(false);
+ };
+
+ return (
+ ) => e.preventDefault() : undefined}
+ >
+
+ {!isOpenBoardTitle ? (
+
) => {
+ event.preventDefault();
+ setIsOpenBoardTitle(true);
+ }}
+ >
+ {title}
+
+ ) : (
+
+ ) => event.preventDefault()}
+ />
+
+
+ )}
+
+ {!isOpenBoardDescr ? (
+
) => {
+ event.preventDefault();
+ setIsOpenBoardDescr(true);
+ }}
+ >
+ {description}
+
+ ) : (
+
+
+ )}
+
+
+
+
+
+
+
+
+ );
+};
+
+export default BoardsItem;
diff --git a/src/pages/boards/typesBoards/TypesBoards.tsx b/src/pages/boards/typesBoards/TypesBoards.tsx
new file mode 100644
index 0000000..77f58d1
--- /dev/null
+++ b/src/pages/boards/typesBoards/TypesBoards.tsx
@@ -0,0 +1,9 @@
+export interface BoardsTypes {
+ title: string;
+ id: string;
+ order?: number;
+ description: string;
+ isActiveModal: (shouldShowModal: boolean) => void;
+ getDeletedBoard: (boardTitle: string, boardId: string) => void;
+ activeModalProps: boolean;
+}
diff --git a/src/pages/index.ts b/src/pages/index.ts
new file mode 100644
index 0000000..e411f2f
--- /dev/null
+++ b/src/pages/index.ts
@@ -0,0 +1,9 @@
+import { Board } from './board/Board';
+import { Boards } from './boards/Boards';
+import { NotFound } from './notfound/NotFound';
+import SignIn from './signIn/SignIn';
+import SignUp from './signUp/SignUp';
+import { UserProfile } from './userProfile/UserProfile';
+import { WelcomePage } from './welcomePage/WelcomePage';
+
+export { Board, Boards, NotFound, SignIn, SignUp, WelcomePage, UserProfile };
diff --git a/src/pages/notfound/NotFound.scss b/src/pages/notfound/NotFound.scss
new file mode 100644
index 0000000..5c6eda0
--- /dev/null
+++ b/src/pages/notfound/NotFound.scss
@@ -0,0 +1 @@
+@import '../../sass/Constants.scss';
diff --git a/src/pages/notfound/NotFound.tsx b/src/pages/notfound/NotFound.tsx
new file mode 100644
index 0000000..335377f
--- /dev/null
+++ b/src/pages/notfound/NotFound.tsx
@@ -0,0 +1,13 @@
+import { FC } from 'react';
+
+import './NotFound.scss';
+
+const NotFound: FC = () => {
+ return (
+ <>
+
+ >
+ );
+};
+
+export { NotFound };
diff --git a/src/pages/signIn/SignIn.tsx b/src/pages/signIn/SignIn.tsx
new file mode 100644
index 0000000..3b26e21
--- /dev/null
+++ b/src/pages/signIn/SignIn.tsx
@@ -0,0 +1,108 @@
+import React, { useState } from 'react';
+import { useForm } from 'react-hook-form';
+import { useAppDispatch, useAppSelector } from '../../app/hooks';
+import { loginUser } from '../../reducers/auth';
+import { useNavigate } from 'react-router-dom';
+import { PATHS } from '../../shared/constants/routes';
+import { useSigninMutation } from '../../app/RtkQuery';
+import { SigninType } from '../../app/apiTypes';
+import { saveTokenToLS } from '../../features/ls-load-save';
+import { Modal } from '../../components';
+import { ErrorSign } from '../../components/modal/components';
+import { localizationObj } from '../../features/localization';
+
+import '../signUp/SignUp.scss';
+
+const SignIn = (): JSX.Element => {
+ const navigate = useNavigate();
+ const dispatch = useAppDispatch();
+ const [signinUser] = useSigninMutation();
+ const { lang } = useAppSelector((state) => state.langStorage);
+ const [activeModal, setActiveModal] = useState(false);
+ const [errorMsg, setErrorMsg] = useState('');
+
+ const {
+ register,
+ handleSubmit,
+ reset,
+ formState: { errors },
+ } = useForm({
+ mode: 'onSubmit',
+ reValidateMode: 'onChange',
+ defaultValues: { login: '', password: '' },
+ });
+
+ const onSubmit = async (user: SigninType) => {
+ signinUser(user)
+ .unwrap()
+ .then(({ token }) => {
+ dispatch(loginUser(token));
+ saveTokenToLS(token);
+ })
+ .then(() => {
+ reset();
+ navigate(PATHS.boards, { replace: true });
+ })
+ .catch((error) => {
+ setActiveModal(true);
+ setErrorMsg(error.data.message);
+ });
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default SignIn;
diff --git a/src/pages/signUp/SignUp.scss b/src/pages/signUp/SignUp.scss
new file mode 100644
index 0000000..6805c5b
--- /dev/null
+++ b/src/pages/signUp/SignUp.scss
@@ -0,0 +1,96 @@
+@import '../../sass/Constants.scss';
+
+.signup {
+ display: flex;
+ justify-content: center;
+ flex-direction: column;
+ width: 45%;
+ margin: $base * 2 auto;
+ padding: $base * 2;
+ font-size: $fs-xxxl;
+ border-radius: $base * 3;
+ box-shadow: 0.063rem 0.063rem 0.063rem $color-sale, 0 0 0.06em $color-dark, 0 0 0.06em $color-dark;
+
+ @media (max-width: $breakpoint-lg-min) {
+ width: 50%;
+ }
+
+ @media (max-width: $breakpoint-md-min) {
+ width: 65%;
+ }
+
+ @media (max-width: $breakpoint-xs-min) {
+ width: 95%;
+ }
+}
+
+.form__error {
+ font-size: $fs-l;
+ color: $color-error;
+}
+
+.form__title {
+ @include disableSelection;
+ font-size: $fs-xxxxl;
+ font-weight: $fw-bold;
+ line-height: $base * 10;
+ text-align: center;
+ text-transform: uppercase;
+ text-shadow: 0.063rem 0.063rem 0.063rem $color-sale, 0 0 0.06em $color-blue-light,
+ 0 0 0.06em $color-blue-light;
+ color: $color-purple;
+}
+
+.form__nickname,
+.form__password {
+ display: grid;
+ grid-template-rows: 1fr 1fr 0.55fr;
+}
+
+.signup__password,
+.signup__name {
+ @include poppins;
+ width: 100%;
+ padding: 0 $base * 2;
+ font-size: $fs-xxxl;
+ background-color: $color-white;
+ border: 0.063rem solid $color-dark;
+
+ @media (max-width: $breakpoint-md-min) {
+ font-size: $fs-xxl;
+ }
+}
+
+.form__label-tittle {
+ font-size: $fs-xxxl;
+ font-weight: $fw-semi-bold;
+ color: $color-dark;
+ text-shadow: 0.063rem 0.063rem 0.063rem $color-dark-02, 0 0 0.06em $color-dark-02,
+ 0 0 0.06em $color-dark-02;
+
+ @media (max-width: $breakpoint-md-min) {
+ font-size: $fs-xxl;
+ }
+}
+
+.form__submit {
+ @include poppins;
+ background-color: $color-white;
+ border: 0.125rem solid $color-dark;
+ border-radius: $base;
+ font-size: $fs-xxl;
+ width: 50%;
+ margin: 0 auto;
+ transition: $transition-01;
+
+ &:hover {
+ border-radius: $base * 3;
+ box-shadow: inset 0px 0px $base * 4 rgb(201, 255, 209);
+ cursor: pointer;
+ }
+
+ &:active {
+ transform: translate(0.063rem, 0.063rem);
+ transition: $transition-01;
+ }
+}
diff --git a/src/pages/signUp/SignUp.tsx b/src/pages/signUp/SignUp.tsx
new file mode 100644
index 0000000..754848b
--- /dev/null
+++ b/src/pages/signUp/SignUp.tsx
@@ -0,0 +1,166 @@
+import React, { useRef, useState } from 'react';
+import { useForm } from 'react-hook-form';
+import { useAppDispatch, useAppSelector } from '../../app/hooks';
+import { loginUser } from '../../reducers/auth';
+import './SignUp.scss';
+import { useNavigate } from 'react-router-dom';
+import { PATHS } from '../../shared/constants/routes';
+import { useSigninMutation, useSignupMutation } from '../../app/RtkQuery';
+import { SignupType } from '../../app/apiTypes';
+import { saveTokenToLS } from '../../features/ls-load-save';
+import { Modal } from '../../components';
+import { ErrorSign } from '../../components/modal/components';
+import { localizationObj } from '../../features/localization';
+
+export type SignUpValues = {
+ name: string;
+ login: string;
+ password: string;
+ repeatPassword: string;
+};
+
+const SignUp = (): JSX.Element => {
+ const navigate = useNavigate();
+ const dispatch = useAppDispatch();
+ const [signupUser] = useSignupMutation();
+ const [signinUser] = useSigninMutation();
+ const { lang } = useAppSelector((state) => state.langStorage);
+ const [activeModal, setActiveModal] = useState(false);
+ const [errorMsg, setErrorMsg] = useState('');
+
+ const {
+ register,
+ handleSubmit,
+ reset,
+ watch,
+ formState: { errors },
+ } = useForm({
+ mode: 'onSubmit',
+ reValidateMode: 'onChange',
+ defaultValues: { name: '', login: '', password: '', repeatPassword: '' },
+ });
+
+ const password = useRef({});
+ password.current = watch('password', '');
+
+ const onSubmit = async ({ name, login, password }: SignUpValues) => {
+ const user: SignupType = {
+ name,
+ login,
+ password,
+ };
+ signupUser(user)
+ .unwrap()
+ .then(() => {
+ return signinUser({ login, password }).unwrap();
+ })
+ .then(({ token }) => {
+ dispatch(loginUser(token));
+ saveTokenToLS(token);
+ })
+ .then(() => {
+ reset();
+ navigate(PATHS.boards, { replace: true });
+ })
+ .catch((error) => {
+ setActiveModal(true);
+ setErrorMsg(error.data.message);
+ });
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+ >
+ );
+};
+
+export default SignUp;
diff --git a/src/pages/userProfile/UserProfile.scss b/src/pages/userProfile/UserProfile.scss
new file mode 100644
index 0000000..6879238
--- /dev/null
+++ b/src/pages/userProfile/UserProfile.scss
@@ -0,0 +1,99 @@
+@import '../../sass/Constants.scss';
+
+.user-profile {
+ display: flex;
+ justify-content: center;
+ flex-direction: column;
+ row-gap: $base * 2;
+ width: 45%;
+ margin: $base * 2 auto;
+ padding: $base * 2;
+ font-size: $fs-xxxl;
+ border-radius: $base * 3;
+ box-shadow: 0.063rem 0.063rem 0.063rem $color-sale, 0 0 0.06em $color-dark, 0 0 0.06em $color-dark;
+
+ @media (max-width: $breakpoint-lg-min) {
+ width: 50%;
+ }
+
+ @media (max-width: $breakpoint-md-min) {
+ width: 65%;
+ }
+
+ @media (max-width: $breakpoint-xs-min) {
+ width: 95%;
+ }
+}
+
+.form__title {
+ @include disableSelection;
+ font-size: $fs-xxxxl;
+ font-weight: $fw-bold;
+ line-height: $base * 10;
+ text-align: center;
+ text-transform: uppercase;
+ text-shadow: 0.063rem 0.063rem 0.063rem $color-sale, 0 0 0.06em $color-blue-light,
+ 0 0 0.06em $color-blue-light;
+ color: $color-purple;
+}
+
+.user-profile-name {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: $base * 4;
+}
+
+.user-profile__field {
+ font-size: $fs-xxxl;
+ font-weight: $fw-semi-bold;
+ color: $color-dark;
+ text-shadow: 0.063rem 0.063rem 0.063rem $color-dark-02, 0 0 0.06em $color-dark-02,
+ 0 0 0.06em $color-dark-02;
+
+ @media (max-width: $breakpoint-md-min) {
+ font-size: $fs-xxl;
+ }
+}
+
+.user-profile__value {
+ font-size: $fs-xxxl;
+ font-weight: $fw-semi-bold;
+ color: $color-dark;
+ text-shadow: 0.063rem 0.063rem 0.063rem $color-dark-02, 0 0 0.06em $color-dark-02,
+ 0 0 0.06em $color-dark-02;
+ text-align: end;
+
+ @media (max-width: $breakpoint-md-min) {
+ font-size: $fs-xxl;
+ }
+}
+
+.user-profile__buttons {
+ display: flex;
+ align-items: center;
+ flex-direction: column;
+ row-gap: $base * 2;
+}
+
+.user-profile__button {
+ @include poppins;
+ background-color: $color-white;
+ border: 0.125rem solid $color-dark;
+ border-radius: $base;
+ font-size: $fs-xxl;
+ width: 100%;
+ margin: 0 auto;
+ transition: $transition-01;
+
+ &:hover {
+ border-radius: $base * 3;
+ box-shadow: inset 0px 0px $base * 4 rgb(201, 255, 209);
+ cursor: pointer;
+ }
+
+ &:active {
+ transform: translate(0.063rem, 0.063rem);
+ transition: $transition-01;
+ }
+}
diff --git a/src/pages/userProfile/UserProfile.tsx b/src/pages/userProfile/UserProfile.tsx
new file mode 100644
index 0000000..b8cb608
--- /dev/null
+++ b/src/pages/userProfile/UserProfile.tsx
@@ -0,0 +1,308 @@
+import React, { FC, useEffect, useState } from 'react';
+import { useForm } from 'react-hook-form';
+import { useAppDispatch, useAppSelector } from '../../app/hooks';
+import {
+ useDeleteUserMutation,
+ useSigninMutation,
+ useUpdateUserMutation,
+} from '../../app/RtkQuery';
+import { Modal } from '../../components';
+import { ErrorSign } from '../../components/modal/components';
+import { localizationObj } from '../../features/localization';
+import { saveTokenToLS } from '../../features/ls-load-save';
+import { loginUser, logoutUser } from '../../reducers/auth';
+import { setEmptyUser } from '../../reducers/userReducer';
+import { ChangeTitleBtns } from '../../components/buttons';
+import './UserProfile.scss';
+
+export type UserEditValues = {
+ name: string;
+ login: string;
+ oldpassword: string;
+ newpassword: string;
+};
+
+const UserProfile: FC = () => {
+ const { userId, userName, userLogin } = useAppSelector((state) => state.userStorage);
+ const [updateUser] = useUpdateUserMutation();
+ const [signinUser] = useSigninMutation();
+ const [deleteUser] = useDeleteUserMutation();
+ const dispatch = useAppDispatch();
+ const [activeModal, setActiveModal] = useState(false);
+ const [errorMsg, setErrorMsg] = useState('');
+ const [deleteMsg, setDeleteMsg] = useState(false);
+ const [successMsg, setSuccessMsg] = useState('');
+ const [isEditing, setIsEditing] = useState(false);
+ const [isPassChanging, setIsPassChanging] = useState(false);
+ const { lang } = useAppSelector((state) => state.langStorage);
+
+ const {
+ register,
+ handleSubmit,
+ reset,
+ formState: { errors },
+ } = useForm({
+ mode: 'onChange',
+ reValidateMode: 'onChange',
+ defaultValues: { name: userName, login: userLogin, oldpassword: '', newpassword: '' },
+ });
+
+ useEffect(() => {
+ if (!activeModal) {
+ setErrorMsg('');
+ setSuccessMsg('');
+ setDeleteMsg(false);
+ }
+ }, [activeModal]);
+
+ const onSubmit = async ({ name, login, oldpassword, newpassword }: UserEditValues) => {
+ const body = {
+ name: isPassChanging ? userName : name,
+ login: isPassChanging ? userLogin : login,
+ password: isPassChanging ? newpassword : oldpassword,
+ };
+ signinUser({ login: userLogin, password: oldpassword })
+ .unwrap()
+ .then(() => {
+ return updateUser({ userId, body }).unwrap();
+ })
+ .then(() => {
+ return signinUser({
+ login: isPassChanging ? userLogin : login,
+ password: isPassChanging ? newpassword : oldpassword,
+ }).unwrap();
+ })
+ .then(({ token }) => {
+ dispatch(loginUser(token));
+ saveTokenToLS(token);
+ })
+ .then(() => {
+ reset({
+ name: name,
+ login: login,
+ oldpassword: '',
+ newpassword: '',
+ });
+ setIsEditing(false);
+ setActiveModal(true);
+ setSuccessMsg(
+ isPassChanging
+ ? `${localizationObj[lang].password} ${localizationObj[lang].updateSuccessful}`
+ : `${localizationObj[lang].user} ${localizationObj[lang].updateSuccessful}`
+ );
+ })
+ .catch((error) => {
+ setActiveModal(true);
+ if (error.data.statusCode === 403) {
+ setErrorMsg(localizationObj[lang].wrongPassword);
+ } else {
+ setErrorMsg(error.data.message);
+ }
+ });
+ };
+
+ const deleteHandler = async () => {
+ deleteUser(userId)
+ .unwrap()
+ .then((data) => {
+ if (!data) {
+ dispatch(logoutUser());
+ dispatch(setEmptyUser());
+ saveTokenToLS('');
+ }
+ })
+ .catch((error) => {
+ setActiveModal(true);
+ setErrorMsg(error.data.message);
+ });
+ };
+
+ useEffect(() => {
+ if (!activeModal) {
+ setSuccessMsg('');
+ setErrorMsg('');
+ }
+ }, [activeModal]);
+
+ return (
+ <>
+ {!isEditing && (
+
+
{localizationObj[lang].profile}
+
+
{localizationObj[lang].name}:
+
{userName}
+
+
+
{localizationObj[lang].login}:
+
{userLogin}
+
+
+
+
+
+
+
+ )}
+
+ {isEditing && (
+
+ )}
+
+
+ <>
+ {!!errorMsg && }
+ {!!successMsg && (
+
+
{localizationObj[lang].success}
+
{successMsg}
+
+ )}
+ {!!deleteMsg && (
+
+
+
+
{`${localizationObj[lang].doYouWantToDelete} '${userName}' ?`}
+
{localizationObj[lang].afterDeleteRedirect}
+
{
+ setActiveModal(false);
+ setDeleteMsg(false);
+ }}
+ />
+
+
+ )}
+ >
+
+ >
+ );
+};
+
+export { UserProfile };
diff --git a/src/pages/welcomePage/WelcomePage.scss b/src/pages/welcomePage/WelcomePage.scss
new file mode 100644
index 0000000..1e9f0ab
--- /dev/null
+++ b/src/pages/welcomePage/WelcomePage.scss
@@ -0,0 +1,119 @@
+@import '../../sass/Constants.scss';
+
+.main {
+ background-color: $color-grey-white;
+}
+
+.welcome-page {
+ .h2 {
+ @include disableSelection;
+ margin-top: $base * 10;
+ margin-bottom: $base * 10;
+ font-size: $fs-xxxxl;
+ text-shadow: 0.063rem 0.063rem 0.063rem $color-purple, 0 0 0.06em $color-purple,
+ 0 0 0.06em $color-purple;
+
+ @media (max-width: $breakpoint-lg-min) {
+ margin-top: 0;
+ font-size: $fs-xxxl;
+ }
+
+ @media (max-width: $breakpoint-md-min) {
+ font-size: $fs-xxl;
+ }
+ }
+
+ .welcome-page__title {
+ font-size: $base * 11;
+ line-height: $base * 15;
+ margin-bottom: $base * 3;
+ }
+
+ .welcome-page__subtitle {
+ margin-bottom: $base * 3;
+ }
+
+ .welcome-page__content {
+ padding: $base * 25 0;
+ width: 50%;
+ margin-right: auto;
+ color: white;
+
+ @media (max-width: $breakpoint-md-min) {
+ padding-bottom: $base * 10;
+ }
+
+ @media (max-width: $breakpoint-sm-min) {
+ width: 90%;
+ margin: auto;
+ text-align: center;
+ }
+ }
+
+ .welcome-page__promo {
+ background: #0a1e3f;
+ background-image: url('../../images/YFNLt5hMncNXRFTyJMBGis_ri01GarCduTNi4PLDKgcMxFbphXYXLyaWJDNkFmu.jpg');
+ background-repeat: no-repeat;
+ background-position: 100% center;
+ background-size: contain;
+ box-shadow: 0px -24px 74px 115px rgb(10 30 63);
+
+ @media (max-width: $breakpoint-lg-min) {
+ background-position: 10rem center;
+ }
+
+ @media (max-width: $breakpoint-sm-min) {
+ background: #0a1e3f;
+ }
+ }
+
+ .team {
+ @include flexCenter;
+ flex-direction: column;
+ gap: $base * 3;
+ width: 80%;
+ margin: 0 auto $base * 3 auto;
+
+ @media (max-width: $breakpoint-lg-min) {
+ width: 97%;
+ }
+ }
+
+ .welcome__container {
+ @include flexSpaceBetween;
+ flex-wrap: wrap;
+ gap: $base * 4;
+ width: 100%;
+ margin-bottom: $base * 4;
+
+ @media (max-width: $breakpoint-lg-min) {
+ justify-content: center;
+ }
+ }
+
+ .about {
+ padding: $base * 15 0;
+ -webkit-box-shadow: 0px -67px 34px 37px rgba(10, 30, 63, 0.1);
+ -moz-box-shadow: 0px -67px 34px 37px rgba(10, 30, 63, 0.1);
+ box-shadow: 0px -67px 34px 37px rgba(10, 30, 63, 0.1);
+ background: rgba(10, 30, 63, 0.1);
+ }
+
+ .about__title {
+ margin: auto;
+ margin-bottom: $base * 7;
+ font-size: $fs-xxxxl;
+ }
+
+ .about__wrapper {
+ flex-direction: column;
+ }
+
+ .about__content {
+ text-align: center;
+
+ @media (max-width: $breakpoint-lg-min) {
+ padding: $base * 3;
+ }
+ }
+}
diff --git a/src/pages/welcomePage/WelcomePage.tsx b/src/pages/welcomePage/WelcomePage.tsx
new file mode 100644
index 0000000..4ceab9a
--- /dev/null
+++ b/src/pages/welcomePage/WelcomePage.tsx
@@ -0,0 +1,64 @@
+import { FC } from 'react';
+import { WelcomeCard } from './components/welcomeCard/WelcomeCard';
+import avatar1 from '../../images/avatar-1.jpg';
+import avatar2 from '../../images/avatar-2.jpg';
+import avatar3 from '../../images/avatar-3.jpg';
+import './WelcomePage.scss';
+import { useAppSelector } from '../../app/hooks';
+import { localizationObj } from '../../features/localization';
+
+const WelcomePage: FC = () => {
+ const { lang } = useAppSelector((state) => state.langStorage);
+
+ return (
+
+
+
+
+
{localizationObj[lang].welcome}
+
{localizationObj[lang].app}
+
{localizationObj[lang].aboutProject}
+
+
+
+
+
+ {localizationObj[lang].meetOurTeam}
+
+
+
+
+
+
+
+
+
+
{localizationObj[lang].courseAbout}
+
+
{localizationObj[lang].course}
+
+
+
+
+ );
+};
+
+export { WelcomePage };
diff --git a/src/pages/welcomePage/components/welcomeCard/WelcomeCard.scss b/src/pages/welcomePage/components/welcomeCard/WelcomeCard.scss
new file mode 100644
index 0000000..ae5cd56
--- /dev/null
+++ b/src/pages/welcomePage/components/welcomeCard/WelcomeCard.scss
@@ -0,0 +1,113 @@
+@import '../../../../sass/Constants.scss';
+
+.welcome__card {
+ position: relative;
+ width: 30%;
+ text-align: center;
+
+ @media (max-width: $breakpoint-lg-min) {
+ width: 45%;
+ }
+
+ @media (max-width: $breakpoint-sm-min) {
+ width: 80%;
+ }
+}
+
+.card-img {
+ @include disableSelection;
+ position: relative;
+ width: 90%;
+ height: auto;
+ border-radius: 50%;
+ border: 0.063rem solid $color-dark-03;
+ box-shadow: rgba(136, 165, 191, 0.6) $base 0.125rem $base * 3 0px,
+ rgba(255, 255, 255, 0.8) -0.313rem -0.125rem $base * 3 0px;
+ z-index: 2;
+ pointer-events: none;
+}
+
+.welcome-card__container {
+ margin: -40% auto 0 auto;
+ padding: 40% 0 0 0;
+ background-color: $color-white;
+ border-top-right-radius: $base * 4;
+ border-top-left-radius: $base * 4;
+ border-bottom-right-radius: $base * 2;
+ border-bottom-left-radius: $base * 2;
+ box-shadow: $color-dark-03 0px $base $base * 3;
+
+ .h3 {
+ font-size: $fs-xl;
+ font-weight: $fw-bold;
+ letter-spacing: 0.06em;
+ color: $color-dark;
+ }
+
+ .h4 {
+ font-size: $fs-l;
+ font-weight: $fw-semi-bold;
+ }
+}
+
+.card-info {
+ text-align: center;
+}
+
+.card__list {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: $base * 3;
+ padding: $base 0;
+ background-color: $color-purple-09;
+ border-bottom-right-radius: $base * 2;
+ border-bottom-left-radius: $base * 2;
+}
+
+.card__list-item {
+ width: $base * 12;
+ height: $base * 12;
+ border-radius: 50%;
+ background-color: $color-white;
+ transition: $transition;
+
+ &:hover {
+ background-color: $color-grey-07;
+ transition: $transition;
+ }
+
+ @media (max-width: $breakpoint-xs-min) {
+ width: $base * 6;
+ height: $base * 6;
+ }
+}
+
+.card-link {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: $base * 12;
+ height: $base * 12;
+ background-size: 60%;
+ background-position: center;
+ background-repeat: no-repeat;
+ border-radius: 50%;
+
+ @media (max-width: $breakpoint-xs-min) {
+ width: $base * 6;
+ height: $base * 6;
+ }
+}
+
+.github {
+ background-image: url('../../../../images/icons/github.svg');
+}
+
+.linkedin {
+ background-image: url('../../../../images/icons/linkedin.svg');
+}
+
+.instagram {
+ background-image: url('../../../../images/icons/instagram.svg');
+}
diff --git a/src/pages/welcomePage/components/welcomeCard/WelcomeCard.tsx b/src/pages/welcomePage/components/welcomeCard/WelcomeCard.tsx
new file mode 100644
index 0000000..29a9e07
--- /dev/null
+++ b/src/pages/welcomePage/components/welcomeCard/WelcomeCard.tsx
@@ -0,0 +1,43 @@
+import { FC } from 'react';
+
+import './WelcomeCard.scss';
+
+interface WelcomeCardProps {
+ src: string;
+ name: string;
+ specialization: string;
+ github: string;
+ linkedin: string;
+}
+
+const WelcomeCard: FC = ({ src, name, specialization, github, linkedin }) => {
+ return (
+
+

+
+
{name}
+
{specialization}
+
+
+
+ );
+};
+
+export { WelcomeCard };
diff --git a/src/react-app-env.d.ts b/src/react-app-env.d.ts
new file mode 100644
index 0000000..6431bc5
--- /dev/null
+++ b/src/react-app-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/src/reducers/auth.ts b/src/reducers/auth.ts
new file mode 100644
index 0000000..ccb3a98
--- /dev/null
+++ b/src/reducers/auth.ts
@@ -0,0 +1,28 @@
+import { createSlice } from '@reduxjs/toolkit';
+import { loadTokenFromLS } from '../features/ls-load-save';
+
+interface AuthState {
+ userToken: string;
+}
+
+const userTokenFromLS = loadTokenFromLS();
+
+const initialAuthState: AuthState = {
+ userToken: userTokenFromLS,
+};
+
+const authSlice = createSlice({
+ name: 'auth',
+ initialState: initialAuthState,
+ reducers: {
+ loginUser(state, action: { payload: string }) {
+ state.userToken = action.payload;
+ },
+ logoutUser(state) {
+ state.userToken = '';
+ },
+ },
+});
+
+export const { loginUser, logoutUser } = authSlice.actions;
+export default authSlice.reducer;
diff --git a/src/reducers/langReducer.ts b/src/reducers/langReducer.ts
new file mode 100644
index 0000000..3a6204f
--- /dev/null
+++ b/src/reducers/langReducer.ts
@@ -0,0 +1,27 @@
+import { createSlice } from '@reduxjs/toolkit';
+import { loadLangFromLS } from '../features/ls-load-save';
+
+type LangState = {
+ lang: LangRuEn;
+};
+
+export type LangRuEn = 'RU' | 'EN';
+
+const langFromLS = loadLangFromLS();
+
+const initialUserState: LangState = {
+ lang: langFromLS,
+};
+
+const langSlice = createSlice({
+ name: 'langReducer',
+ initialState: initialUserState,
+ reducers: {
+ setLang(state, action: { payload: LangRuEn }) {
+ state.lang = action.payload;
+ },
+ },
+});
+
+export const { setLang } = langSlice.actions;
+export default langSlice.reducer;
diff --git a/src/reducers/userReducer.ts b/src/reducers/userReducer.ts
new file mode 100644
index 0000000..727cc64
--- /dev/null
+++ b/src/reducers/userReducer.ts
@@ -0,0 +1,29 @@
+import { createSlice } from '@reduxjs/toolkit';
+
+interface UserState {
+ userName: string;
+ userId: string;
+ userLogin: string;
+}
+
+const initialUserState: UserState = {
+ userName: '',
+ userId: '',
+ userLogin: '',
+};
+
+const userSlice = createSlice({
+ name: 'userReducer',
+ initialState: initialUserState,
+ reducers: {
+ setUserData(state, action: { payload: UserState }) {
+ return action.payload;
+ },
+ setEmptyUser() {
+ return initialUserState;
+ },
+ },
+});
+
+export const { setUserData, setEmptyUser } = userSlice.actions;
+export default userSlice.reducer;
diff --git a/src/sass/Constants.scss b/src/sass/Constants.scss
new file mode 100644
index 0000000..e25d931
--- /dev/null
+++ b/src/sass/Constants.scss
@@ -0,0 +1,164 @@
+// === MIXIN ===
+
+@mixin scroll {
+ &::-webkit-scrollbar {
+ width: $base;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background-color: $color-dark-02;
+ border-radius: $base;
+ }
+
+ &::-webkit-scrollbar-track {
+ box-shadow: inset 0 0 $base grey;
+ border-radius: $base * 2;
+ height: $base * 2;
+ background-color: $color-grey-white;
+ }
+}
+
+@mixin poppins {
+ font-family: 'Poppins', sans-serif;
+}
+
+@mixin disableSelection {
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+@mixin flexSpaceEvenly {
+ display: flex;
+ align-items: center;
+ justify-content: space-evenly;
+}
+
+@mixin flexSpaceAround {
+ display: flex;
+ align-items: center;
+ justify-content: space-around;
+}
+
+@mixin flexSpaceBetween {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+@mixin flexStart {
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+}
+
+@mixin flexCenter {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+@mixin flexEnd {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+}
+
+// === / MIXIN ===
+
+// === FUNCTIONS ===
+
+@function strip-unit($num) {
+ @return $num / ($num * 0 + 1);
+}
+
+@function rem($num) {
+ @return (strip-unit($num) / 16) * 1rem;
+}
+
+// === / FUNCTIONS ===
+
+// === VARIABLES ===
+
+$base: rem(5px);
+
+$fw-regular: 400;
+$fw-medium: 500;
+$fw-semi-bold: 600;
+$fw-bold: 700;
+
+$fs-xxs: rem(11px);
+$fs-xs: rem(12px);
+$fs-s: rem(13px);
+$fs-m: rem(14px);
+$fs-l: rem(16px);
+$fs-xl: rem(22px);
+$fs-xxl: rem(26px);
+$fs-xxxl: rem(36px);
+$fs-xxxxl: rem(50px);
+
+$transition-01: 0.1s;
+$transition-02: 0.2s;
+$transition: 0.3s;
+
+$color-dark: rgba(18, 18, 18, 1);
+$color-dark-02: rgba(18, 18, 18, 0.2);
+$color-dark-03: rgba(18, 18, 18, 0.3);
+$color-dark-04: rgba(18, 18, 18, 0.4);
+$color-dark-05: rgba(18, 18, 18, 0.5);
+$color-dark-06: rgba(18, 18, 18, 0.6);
+$color-dark-07: rgba(18, 18, 18, 0.7);
+$color-dark-08: rgba(18, 18, 18, 0.8);
+$color-dark-09: rgba(18, 18, 18, 0.9);
+$color-grey: rgba(229, 229, 229, 1);
+$color-grey-white: rgba(248, 248, 248, 1);
+$color-grey-07: rgba(243, 243, 243, 0.7);
+$color-white: rgba(255, 255, 255, 1);
+$color-white-02: rgba(255, 255, 255, 0.2);
+$color-white-03: rgba(255, 255, 255, 0.3);
+$color-white-04: rgba(255, 255, 255, 0.4);
+$color-white-05: rgba(255, 255, 255, 0.5);
+$color-white-06: rgba(255, 255, 255, 0.6);
+$color-white-07: rgba(255, 255, 255, 0.7);
+$color-white-08: rgba(255, 255, 255, 0.8);
+$color-white-09: rgba(255, 255, 255, 0.9);
+$color-sale: rgba(233, 30, 99, 1);
+$color-yellow: rgba(240, 204, 132, 1);
+$color-error: rgba(214, 19, 19, 1);
+$color-ok: rgba(11, 177, 127, 1);
+$color-grey-dark: rgba(156, 156, 156, 1);
+$color-orange: rgba(211, 118, 12, 1);
+$color-green: rgba(0, 255, 119, 1);
+$color-blue: rgba(0, 3, 51, 1);
+$color-blue-05: rgba(0, 3, 51, 0.5);
+$color-blue-08: rgba(0, 3, 51, 0.8);
+$color-blue-09: rgba(0, 3, 51, 0.9);
+$color-blue-light: rgba(33, 150, 243, 1);
+$color-yellow: rgba(233, 192, 10, 1);
+$color-yellow-dark: rgba(235, 159, 19, 0.25);
+$color-purple: rgba(101, 81, 243, 1);
+$color-purple-06: rgba(101, 81, 243, 0.6);
+$color-purple-09: rgba(101, 81, 243, 0.9);
+
+$breakpoint-xxs-min: 320px;
+$breakpoint-xs-min: 480px;
+$breakpoint-sm-min: 576px;
+$breakpoint-md-min: 768px;
+$breakpoint-lg-min: 992px;
+$breakpoint-xl-min: 1200px;
+$breakpoint-xxl-min: 1440px;
+
+$breakpoint-xxxs-max: $breakpoint-xxs-min - 1px;
+$breakpoint-xxs-max: $breakpoint-xs-min - 1px;
+$breakpoint-xs-max: $breakpoint-sm-min - 1px;
+$breakpoint-sm-max: $breakpoint-md-min - 1px;
+$breakpoint-md-max: $breakpoint-lg-min - 1px;
+$breakpoint-lg-max: $breakpoint-xl-min - 1px;
+$breakpoint-xl-max: $breakpoint-xxl-min - 1px;
+
+$calcWidth: 100%;
+
+// === / VARIABLES ===
diff --git a/src/sass/Fonts.scss b/src/sass/Fonts.scss
new file mode 100644
index 0000000..4da094a
--- /dev/null
+++ b/src/sass/Fonts.scss
@@ -0,0 +1,4 @@
+@include font('Poppins', 'Poppins-Regular', '400', 'normal');
+@include font('Poppins', 'Poppins-Medium', '500', 'normal');
+@include font('Poppins', 'Poppins-SemiBold', '600', 'normal');
+@include font('Poppins', 'Poppins-Bold', '700', 'normal');
diff --git a/src/setupTests.ts b/src/setupTests.ts
new file mode 100644
index 0000000..74b1a27
--- /dev/null
+++ b/src/setupTests.ts
@@ -0,0 +1,5 @@
+// jest-dom adds custom jest matchers for asserting on DOM nodes.
+// allows you to do things like:
+// expect(element).toHaveTextContent(/react/i)
+// learn more: https://github.com/testing-library/jest-dom
+import '@testing-library/jest-dom/extend-expect';
diff --git a/src/shared/constants/routes.ts b/src/shared/constants/routes.ts
new file mode 100644
index 0000000..71cebcb
--- /dev/null
+++ b/src/shared/constants/routes.ts
@@ -0,0 +1,19 @@
+interface IPATH {
+ main: string;
+ boards: string;
+ board: string;
+ signIn: string;
+ signUp: string;
+ userProfile: string;
+ notFound: string;
+}
+
+export const PATHS: IPATH = {
+ main: '/',
+ boards: '/boards',
+ board: '/boards/:id',
+ signIn: '/signin',
+ signUp: '/signup',
+ userProfile: '/userprofile',
+ notFound: '*',
+};
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..a273b0c
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "lib": [
+ "dom",
+ "dom.iterable",
+ "esnext"
+ ],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "noFallthroughCasesInSwitch": true,
+ "module": "esnext",
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx"
+ },
+ "include": [
+ "src"
+ ]
+}