├── .eslintrc.js
├── .firebaserc
├── .github
└── workflows
│ └── deploy-client.yml
├── .gitignore
├── .prettierrc
├── README.md
├── firebase.json
├── package.json
├── pre-build.js
├── public
├── favicon.ico
├── favicon.png
├── index.html
├── manifest.json
└── robots.txt
├── src
├── appConstants
│ ├── api.ts
│ ├── colors.ts
│ └── index.ts
├── assets
│ ├── fonts
│ │ ├── Poppins-Bold-700.eot
│ │ ├── Poppins-Bold-700.otf
│ │ ├── Poppins-Bold-700.ttf
│ │ ├── Poppins-Bold-700.woff
│ │ ├── Poppins-Bold-700.woff2
│ │ ├── Poppins-Medium-500.eot
│ │ ├── Poppins-Medium-500.otf
│ │ ├── Poppins-Medium-500.ttf
│ │ ├── Poppins-Medium-500.woff
│ │ ├── Poppins-Medium-500.woff2
│ │ ├── Poppins-Regular-400.eot
│ │ ├── Poppins-Regular-400.otf
│ │ ├── Poppins-Regular-400.ttf
│ │ ├── Poppins-Regular-400.woff
│ │ ├── Poppins-Regular-400.woff2
│ │ ├── Poppins-SemiBold-600.eot
│ │ ├── Poppins-SemiBold-600.otf
│ │ ├── Poppins-SemiBold-600.ttf
│ │ ├── Poppins-SemiBold-600.woff
│ │ └── Poppins-SemiBold-600.woff2
│ ├── images
│ │ ├── editProfileExampleEN.png
│ │ ├── editProfileExampleRU.png
│ │ ├── myTeamExampleEN.png
│ │ ├── myTeamExampleRU.png
│ │ ├── teamActionsExampleEN.png
│ │ └── teamActionsExampleRU.png
│ └── svg
│ │ ├── check.svg
│ │ ├── chevron-arrow.svg
│ │ ├── copy.svg
│ │ ├── copy2clip.svg
│ │ ├── coursesSelectArrow.svg
│ │ ├── cross-small.svg
│ │ ├── cross.svg
│ │ ├── edit.svg
│ │ ├── filterIcon.svg
│ │ ├── headerActiveElement.svg
│ │ ├── info.svg
│ │ ├── loginImage.svg
│ │ ├── menuToggle.svg
│ │ ├── paginateArrowLeft.svg
│ │ ├── paginateArrowRight.svg
│ │ ├── plus.svg
│ │ ├── rslogo.svg
│ │ ├── team-header-background-pattern.svg
│ │ ├── team-header-decorations.svg
│ │ └── teams-man.svg
├── components
│ ├── App
│ │ ├── index.tsx
│ │ └── styled.ts
│ ├── CommonSelectList
│ │ ├── index.tsx
│ │ └── styled.ts
│ ├── CourseField
│ │ ├── index.tsx
│ │ └── styled.ts
│ ├── ErrorBoundary
│ │ ├── index.tsx
│ │ └── styled.ts
│ ├── ErrorModal
│ │ └── index.tsx
│ ├── FilterForm
│ │ ├── filterFormFields.ts
│ │ ├── index.tsx
│ │ └── styled.ts
│ ├── FilterSelect
│ │ └── index.tsx
│ ├── Footer
│ │ ├── components
│ │ │ ├── FooterContent
│ │ │ │ └── index.tsx
│ │ │ └── index.ts
│ │ ├── index.tsx
│ │ └── styled.ts
│ ├── Header
│ │ ├── components
│ │ │ ├── BurgerMenu
│ │ │ │ ├── index.tsx
│ │ │ │ └── styled.ts
│ │ │ ├── MenuWrapper
│ │ │ │ ├── components
│ │ │ │ │ ├── CoursesSelect
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── LangSelect
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ └── Nav
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ └── styled.ts
│ │ │ │ ├── index.tsx
│ │ │ │ └── styled.ts
│ │ │ └── index.ts
│ │ ├── index.tsx
│ │ └── styled.ts
│ ├── InputField
│ │ ├── index.tsx
│ │ └── styled.ts
│ ├── Loader
│ │ ├── index.tsx
│ │ └── styled.ts
│ ├── Modal
│ │ ├── index.module.css
│ │ └── index.tsx
│ ├── ModalCreateEditTeam
│ │ └── index.tsx
│ ├── ModalCreated
│ │ └── index.tsx
│ ├── ModalEditCourse
│ │ └── index.tsx
│ ├── ModalExpel
│ │ └── index.tsx
│ ├── ModalJoin
│ │ └── index.tsx
│ ├── Pagination
│ │ ├── index.tsx
│ │ └── style.css
│ ├── PrivateRoute
│ │ └── index.tsx
│ ├── SelectField
│ │ ├── index.tsx
│ │ └── styled.ts
│ ├── TablePopup
│ │ ├── index.tsx
│ │ └── styled.ts
│ ├── TourGuide
│ │ ├── index.tsx
│ │ ├── style.css
│ │ ├── styled.ts
│ │ └── tourConfig.tsx
│ └── index.ts
├── graphql
│ ├── mutations
│ │ ├── addUserToTeamMutation.ts
│ │ ├── createCourseMutation.ts
│ │ ├── createTeamMutation.ts
│ │ ├── index.ts
│ │ ├── removeUserFromCourseMutation.ts
│ │ ├── removeUserFromTeamMutation.ts
│ │ ├── sortStudentsMutation.ts
│ │ ├── updUserMutation.ts
│ │ ├── updateCourseMutation.ts
│ │ └── updateTeamMutation.ts
│ └── queries
│ │ ├── coursesQuery.ts
│ │ ├── index.ts
│ │ ├── teamsQuery.ts
│ │ ├── usersQuery.ts
│ │ └── whoAmIQuery.ts
├── hooks
│ └── graphql
│ │ ├── index.ts
│ │ ├── mutations
│ │ ├── useAddUserToTeamMutation.ts
│ │ ├── useCreateCourseMutation.ts
│ │ ├── useCreateTeamMutation.ts
│ │ ├── useExpelUserFromTeamMutation.ts
│ │ ├── useRemoveUserFromCourseMutation.ts
│ │ ├── useRemoveUserFromTeamMutation.ts
│ │ ├── useSortStudentsMutation.ts
│ │ ├── useUpdUserMutation.ts
│ │ ├── useUpdateCourseMutation.ts
│ │ └── useUpdateTeamMutation.ts
│ │ └── queries
│ │ ├── useCoursesQuery.ts
│ │ ├── useTeamsQuery.ts
│ │ ├── useUsersQuery.ts
│ │ └── useWhoAmIQuery.ts
├── index.tsx
├── modules
│ ├── AdminPage
│ │ ├── components
│ │ │ ├── ContentWrapper
│ │ │ │ ├── components
│ │ │ │ │ ├── AddCourseBlock
│ │ │ │ │ │ ├── components
│ │ │ │ │ │ │ └── InputsBlock
│ │ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ │ └── styled.ts
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ └── styled.ts
│ │ │ │ │ ├── CoursesList
│ │ │ │ │ │ ├── components
│ │ │ │ │ │ │ └── Course
│ │ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ │ └── styled.ts
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ └── styled.ts
│ │ │ │ │ └── ShowCourseSelect
│ │ │ │ │ │ └── index.tsx
│ │ │ │ ├── index.tsx
│ │ │ │ └── styled.ts
│ │ │ └── index.ts
│ │ └── index.tsx
│ ├── EditProfile
│ │ ├── components
│ │ │ └── UserCourseListItem
│ │ │ │ ├── index.tsx
│ │ │ │ └── styled.ts
│ │ ├── formFields.ts
│ │ ├── index.tsx
│ │ └── styled.ts
│ ├── LoginPage
│ │ ├── components
│ │ │ ├── LoginInfoBlock
│ │ │ │ ├── index.tsx
│ │ │ │ └── styled.ts
│ │ │ └── index.ts
│ │ ├── index.tsx
│ │ ├── loginPageMiddleware.ts
│ │ ├── loginPageReducer.ts
│ │ ├── selectors.ts
│ │ └── styled.ts
│ ├── NotFoundPage
│ │ ├── index.tsx
│ │ └── styled.ts
│ ├── StudentsTable
│ │ ├── components
│ │ │ └── Dashboard
│ │ │ │ ├── components
│ │ │ │ ├── TableBody
│ │ │ │ │ ├── components
│ │ │ │ │ │ └── TableRow
│ │ │ │ │ │ │ ├── components
│ │ │ │ │ │ │ └── TableItem
│ │ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ │ └── styled.ts
│ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ └── styled.ts
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ ├── styled.ts
│ │ │ │ │ └── styles.css
│ │ │ │ ├── TableHead
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ └── styled.ts
│ │ │ │ └── index.ts
│ │ │ │ ├── index.tsx
│ │ │ │ └── styled.ts
│ │ ├── index.tsx
│ │ ├── selectors.ts
│ │ ├── studentsTableReducer.ts
│ │ └── styled.ts
│ ├── TeamsList
│ │ ├── components
│ │ │ ├── TeamListModals
│ │ │ │ ├── index.tsx
│ │ │ │ └── useCommonMutations.ts
│ │ │ ├── Teams
│ │ │ │ ├── components
│ │ │ │ │ ├── MemberListToggle
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ └── styled.ts
│ │ │ │ │ ├── MyTeam
│ │ │ │ │ │ ├── components
│ │ │ │ │ │ │ └── MyTeamInfoBlock
│ │ │ │ │ │ │ │ ├── components
│ │ │ │ │ │ │ │ ├── MyTeamInfoLine
│ │ │ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ │ │ └── styled.tsx
│ │ │ │ │ │ │ │ └── NotificationPopup
│ │ │ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ │ │ └── styled.ts
│ │ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ │ └── styled.ts
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ └── styled.ts
│ │ │ │ │ ├── TeamItem
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ └── styled.tsx
│ │ │ │ │ ├── TeamUserTable
│ │ │ │ │ │ ├── components
│ │ │ │ │ │ │ └── TableRow
│ │ │ │ │ │ │ │ ├── components
│ │ │ │ │ │ │ │ ├── ExpelButton
│ │ │ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ │ │ └── styled.ts
│ │ │ │ │ │ │ │ ├── TableCell
│ │ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ └── styled.tsx
│ │ │ │ │ ├── TeamsHeader
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ └── styled.ts
│ │ │ │ │ └── index.ts
│ │ │ │ └── index.tsx
│ │ │ └── index.ts
│ │ ├── index.tsx
│ │ ├── selectors.ts
│ │ ├── styled.ts
│ │ └── teamsListReducer.ts
│ ├── TokenPage
│ │ └── index.tsx
│ ├── TutorialPage
│ │ ├── components
│ │ │ ├── NoteBlock
│ │ │ │ ├── index.tsx
│ │ │ │ └── styled.ts
│ │ │ ├── StepBlock
│ │ │ │ ├── index.tsx
│ │ │ │ └── styled.ts
│ │ │ └── index.ts
│ │ ├── index.tsx
│ │ ├── styled.ts
│ │ └── tutorialPageInfo.tsx
│ └── index.ts
├── react-app-env.d.ts
├── reportWebVitals.ts
├── setupTests.ts
├── store
│ └── index.tsx
├── translation
│ ├── en
│ │ └── en.json
│ ├── resources.ts
│ └── ru
│ │ └── ru.json
├── types.ts
├── typography
│ ├── common.css
│ ├── fonts.css
│ ├── index.ts
│ └── normalize.css
└── utils
│ └── isFieldValid.ts
├── tsconfig.json
└── yarn.lock
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: '@typescript-eslint/parser',
3 | extends: [
4 | 'plugin:react/recommended',
5 | 'plugin:@typescript-eslint/recommended',
6 | 'prettier/@typescript-eslint',
7 | 'plugin:react-hooks/recommended',
8 | 'plugin:prettier/recommended',
9 | ],
10 | plugins: ['@typescript-eslint', 'react', 'prettier', 'react-hooks'],
11 | parserOptions: {
12 | ecmaVersion: 2018,
13 | sourceType: 'module',
14 | ecmaFeatures: {
15 | jsx: true,
16 | },
17 | },
18 |
19 | rules: {
20 | 'react-hooks/rules-of-hooks': 'error',
21 | 'react-hooks/exhaustive-deps': 'warn',
22 | 'comma-dangle': ['error', 'only-multiline'],
23 | 'react/prop-types': 'off',
24 | 'react/display-name': 'off',
25 | '@typescript-eslint/explicit-function-return-type': 'off',
26 | 'prettier/prettier': ['error', { endOfLine: 'auto' }],
27 | '@typescript-eslint/interface-name-prefix': 'off',
28 | '@typescript-eslint/ban-ts-ignore': 'off',
29 | '@typescript-eslint/explicit-module-boundary-types': 'off',
30 | '@typescript-eslint/no-empty-function': 'off',
31 | '@typescript-eslint/no-explicit-any': 'off',
32 | '@typescript-eslint/no-var-reqiures': 'off',
33 | },
34 |
35 | settings: {
36 | react: {
37 | version: 'detect',
38 | },
39 | },
40 | globals: { React: 'writable' },
41 | };
42 |
--------------------------------------------------------------------------------
/.firebaserc:
--------------------------------------------------------------------------------
1 | {
2 | "projects": {
3 | "default": "rss-teams"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/.github/workflows/deploy-client.yml:
--------------------------------------------------------------------------------
1 | name: Build and Deploy
2 | on:
3 | push:
4 | branches:
5 | - master
6 |
7 | jobs:
8 | admin:
9 | name: Deploy PROD
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Checkout Repo
13 | uses: actions/checkout@master
14 | - name: Install Dependencies
15 | run: npm install
16 | - name: Build
17 | run: npm run build
18 | - name: Archive Production Artifact
19 | uses: actions/upload-artifact@master
20 | with:
21 | name: build
22 | path: build
23 | - name: Deploy to Firebase
24 | uses: w9jds/firebase-action@master
25 | with:
26 | args: deploy --only hosting
27 | env:
28 | FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | /.eslintcache*
8 |
9 | # testing
10 | /coverage
11 |
12 | # production
13 | /build
14 | /.firebase
15 |
16 | # misc
17 | .DS_Store
18 | .env.local
19 | .env.development.local
20 | .env.test.local
21 | .env.production.local
22 |
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | .eslintcache*
27 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 |
2 | {
3 | "endOfLine": "auto",
4 | "semi": true,
5 | "singleQuote": true,
6 | "tabWidth": 2,
7 | "trailingComma": "es5",
8 | "printWidth": 100
9 | }
--------------------------------------------------------------------------------
/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "hosting": {
3 | "public": "build",
4 | "ignore": [
5 | "firebase.json",
6 | "**/.*",
7 | "**/node_modules/**"
8 | ],
9 | "rewrites": [
10 | {
11 | "source": "**",
12 | "destination": "/index.html"
13 | }
14 | ]
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rss-teams-fe",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@apollo/client": "^3.3.7",
7 | "@testing-library/jest-dom": "^5.11.4",
8 | "@testing-library/react": "^11.1.0",
9 | "@testing-library/user-event": "^12.1.10",
10 | "@types/jest": "^26.0.15",
11 | "@types/node": "^12.0.0",
12 | "@types/react": "^16.9.53",
13 | "@types/react-dom": "^16.9.8",
14 | "@types/react-router-dom": "^5.1.7",
15 | "@types/react-virtualized-auto-sizer": "^1.0.0",
16 | "@types/react-window": "^1.8.2",
17 | "@types/reactour": "^1.18.1",
18 | "@types/redux-actions": "^2.6.1",
19 | "@types/styled-components": "^5.1.7",
20 | "graphql": "^15.5.0",
21 | "i18next": "^19.9.1",
22 | "react": "^17.0.1",
23 | "react-dom": "^17.0.1",
24 | "react-hook-form": "^6.15.1",
25 | "react-i18next": "^11.8.8",
26 | "react-paginate": "^7.0.0",
27 | "react-redux": "^7.2.2",
28 | "react-router-dom": "^5.2.0",
29 | "react-scripts": "4.0.3",
30 | "react-virtualized-auto-sizer": "^1.0.4",
31 | "react-window": "^1.8.6",
32 | "reactour": "^1.18.3",
33 | "redux": "^4.0.5",
34 | "redux-actions": "^2.6.5",
35 | "redux-thunk": "^2.3.0",
36 | "styled-components": "^5.2.1",
37 | "typescript": "4.2.4",
38 | "web-vitals": "^0.2.4"
39 | },
40 | "scripts": {
41 | "start": "react-scripts start",
42 | "build": "npm run pre-build && react-scripts build",
43 | "test": "react-scripts test",
44 | "eject": "react-scripts eject",
45 | "pre-build": "node pre-build",
46 | "deploy": "firebase deploy",
47 | "lint:eslint": "eslint \"{,!(node_modules)/**/}*.{ts,tsx}\"",
48 | "fix:prettier": "prettier --write \"{,!(node_modules)/**/}*.{ts,tsx}\"",
49 | "fix:eslint": "eslint --fix \"{,!(node_modules)/**/}*.{{ts,tsx}\""
50 | },
51 | "eslintConfig": {
52 | "extends": [
53 | "react-app",
54 | "react-app/jest"
55 | ]
56 | },
57 | "browserslist": {
58 | "production": [
59 | ">0.2%",
60 | "not dead",
61 | "not op_mini all"
62 | ],
63 | "development": [
64 | "last 1 chrome version",
65 | "last 1 firefox version",
66 | "last 1 safari version"
67 | ]
68 | },
69 | "devDependencies": {
70 | "@types/react-paginate": "^6.2.1",
71 | "@types/react-redux": "^7.1.16",
72 | "@types/redux": "^3.6.0",
73 | "@typescript-eslint/eslint-plugin": "^4.14.1",
74 | "@typescript-eslint/parser": "^4.14.1",
75 | "eslint": "^7.19.0",
76 | "eslint-config-prettier": "^7.2.0",
77 | "eslint-plugin-prettier": "^3.3.1",
78 | "eslint-plugin-react": "^7.22.0",
79 | "eslint-plugin-react-hooks": "^4.2.0",
80 | "prettier": "^2.2.1",
81 | "redux-devtools-extension": "^2.13.8"
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/pre-build.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-var-requires */
2 | const fs = require('fs');
3 | require('dotenv').config();
4 |
5 | const BACKEND_LINK = `export const BACKEND_LINK = 'https://rss-teams.herokuapp.com/graphql';
6 | `;
7 |
8 | const AUTH_BACKEND_LINK = `export const AUTH_BACKEND_LINK = 'https://rss-teams.herokuapp.com/auth/github/';
9 | `;
10 |
11 | fs.writeFileSync('./src/appConstants/api.ts', BACKEND_LINK + AUTH_BACKEND_LINK);
12 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rolling-scopes-school/RSS-Teams-FE/87986aad70b2aa2fc7e7ae1b729e7f4963533261/public/favicon.ico
--------------------------------------------------------------------------------
/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rolling-scopes-school/RSS-Teams-FE/87986aad70b2aa2fc7e7ae1b729e7f4963533261/public/favicon.png
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | RSS Teams
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "RSS Teams",
3 | "name": "The Rolling Scopes School Teams",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "favicon.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "favicon.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/appConstants/api.ts:
--------------------------------------------------------------------------------
1 | export const BACKEND_LINK = 'https://rss-teams-dev.herokuapp.com/graphql';
2 | export const AUTH_BACKEND_LINK = 'https://rss-teams-dev.herokuapp.com/auth/github/';
3 |
--------------------------------------------------------------------------------
/src/appConstants/colors.ts:
--------------------------------------------------------------------------------
1 | export const WHITE_COLOR = '#FFFFFF';
2 | export const BG_COLOR = '#F2F8FD';
3 | export const MAIN1_COLOR = '#6550F6'; // violet
4 | export const MAIN1_DARK_COLOR = '#5039EF'; // dark violet
5 | export const MAIN2_COLOR = '#FA6678'; // red
6 | export const MAIN2_LIGHT_COLOR = '#FE7888'; // light red
7 | export const LIGHT_TEXT_COLOR = '#7E96C2';
8 | export const DARK_TEXT_COLOR = '#363D48';
9 | export const OVERLAY_COLOR = '#363D4866';
10 | export const DASHBOARD_HEADER_BG_COLOR = '#E1EEFA';
11 | export const TABLE_SCROLLBAR_BG_COLOR = '#F9F9FD';
12 | export const TABLE_SCROLLBAR_THUMB_COLOR = '#1E33570D';
13 | export const TABLE_POPUP_BORDER_COLOR = '#363D4833';
14 | export const FOOTER_NAMES_COLOR = '#9CA5B5';
15 |
16 | export const ALERT_COLOR = '#FA6678'; // red
17 |
--------------------------------------------------------------------------------
/src/assets/fonts/Poppins-Bold-700.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rolling-scopes-school/RSS-Teams-FE/87986aad70b2aa2fc7e7ae1b729e7f4963533261/src/assets/fonts/Poppins-Bold-700.eot
--------------------------------------------------------------------------------
/src/assets/fonts/Poppins-Bold-700.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rolling-scopes-school/RSS-Teams-FE/87986aad70b2aa2fc7e7ae1b729e7f4963533261/src/assets/fonts/Poppins-Bold-700.otf
--------------------------------------------------------------------------------
/src/assets/fonts/Poppins-Bold-700.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rolling-scopes-school/RSS-Teams-FE/87986aad70b2aa2fc7e7ae1b729e7f4963533261/src/assets/fonts/Poppins-Bold-700.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/Poppins-Bold-700.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rolling-scopes-school/RSS-Teams-FE/87986aad70b2aa2fc7e7ae1b729e7f4963533261/src/assets/fonts/Poppins-Bold-700.woff
--------------------------------------------------------------------------------
/src/assets/fonts/Poppins-Bold-700.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rolling-scopes-school/RSS-Teams-FE/87986aad70b2aa2fc7e7ae1b729e7f4963533261/src/assets/fonts/Poppins-Bold-700.woff2
--------------------------------------------------------------------------------
/src/assets/fonts/Poppins-Medium-500.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rolling-scopes-school/RSS-Teams-FE/87986aad70b2aa2fc7e7ae1b729e7f4963533261/src/assets/fonts/Poppins-Medium-500.eot
--------------------------------------------------------------------------------
/src/assets/fonts/Poppins-Medium-500.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rolling-scopes-school/RSS-Teams-FE/87986aad70b2aa2fc7e7ae1b729e7f4963533261/src/assets/fonts/Poppins-Medium-500.otf
--------------------------------------------------------------------------------
/src/assets/fonts/Poppins-Medium-500.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rolling-scopes-school/RSS-Teams-FE/87986aad70b2aa2fc7e7ae1b729e7f4963533261/src/assets/fonts/Poppins-Medium-500.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/Poppins-Medium-500.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rolling-scopes-school/RSS-Teams-FE/87986aad70b2aa2fc7e7ae1b729e7f4963533261/src/assets/fonts/Poppins-Medium-500.woff
--------------------------------------------------------------------------------
/src/assets/fonts/Poppins-Medium-500.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rolling-scopes-school/RSS-Teams-FE/87986aad70b2aa2fc7e7ae1b729e7f4963533261/src/assets/fonts/Poppins-Medium-500.woff2
--------------------------------------------------------------------------------
/src/assets/fonts/Poppins-Regular-400.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rolling-scopes-school/RSS-Teams-FE/87986aad70b2aa2fc7e7ae1b729e7f4963533261/src/assets/fonts/Poppins-Regular-400.eot
--------------------------------------------------------------------------------
/src/assets/fonts/Poppins-Regular-400.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rolling-scopes-school/RSS-Teams-FE/87986aad70b2aa2fc7e7ae1b729e7f4963533261/src/assets/fonts/Poppins-Regular-400.otf
--------------------------------------------------------------------------------
/src/assets/fonts/Poppins-Regular-400.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rolling-scopes-school/RSS-Teams-FE/87986aad70b2aa2fc7e7ae1b729e7f4963533261/src/assets/fonts/Poppins-Regular-400.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/Poppins-Regular-400.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rolling-scopes-school/RSS-Teams-FE/87986aad70b2aa2fc7e7ae1b729e7f4963533261/src/assets/fonts/Poppins-Regular-400.woff
--------------------------------------------------------------------------------
/src/assets/fonts/Poppins-Regular-400.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rolling-scopes-school/RSS-Teams-FE/87986aad70b2aa2fc7e7ae1b729e7f4963533261/src/assets/fonts/Poppins-Regular-400.woff2
--------------------------------------------------------------------------------
/src/assets/fonts/Poppins-SemiBold-600.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rolling-scopes-school/RSS-Teams-FE/87986aad70b2aa2fc7e7ae1b729e7f4963533261/src/assets/fonts/Poppins-SemiBold-600.eot
--------------------------------------------------------------------------------
/src/assets/fonts/Poppins-SemiBold-600.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rolling-scopes-school/RSS-Teams-FE/87986aad70b2aa2fc7e7ae1b729e7f4963533261/src/assets/fonts/Poppins-SemiBold-600.otf
--------------------------------------------------------------------------------
/src/assets/fonts/Poppins-SemiBold-600.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rolling-scopes-school/RSS-Teams-FE/87986aad70b2aa2fc7e7ae1b729e7f4963533261/src/assets/fonts/Poppins-SemiBold-600.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/Poppins-SemiBold-600.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rolling-scopes-school/RSS-Teams-FE/87986aad70b2aa2fc7e7ae1b729e7f4963533261/src/assets/fonts/Poppins-SemiBold-600.woff
--------------------------------------------------------------------------------
/src/assets/fonts/Poppins-SemiBold-600.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rolling-scopes-school/RSS-Teams-FE/87986aad70b2aa2fc7e7ae1b729e7f4963533261/src/assets/fonts/Poppins-SemiBold-600.woff2
--------------------------------------------------------------------------------
/src/assets/images/editProfileExampleEN.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rolling-scopes-school/RSS-Teams-FE/87986aad70b2aa2fc7e7ae1b729e7f4963533261/src/assets/images/editProfileExampleEN.png
--------------------------------------------------------------------------------
/src/assets/images/editProfileExampleRU.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rolling-scopes-school/RSS-Teams-FE/87986aad70b2aa2fc7e7ae1b729e7f4963533261/src/assets/images/editProfileExampleRU.png
--------------------------------------------------------------------------------
/src/assets/images/myTeamExampleEN.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rolling-scopes-school/RSS-Teams-FE/87986aad70b2aa2fc7e7ae1b729e7f4963533261/src/assets/images/myTeamExampleEN.png
--------------------------------------------------------------------------------
/src/assets/images/myTeamExampleRU.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rolling-scopes-school/RSS-Teams-FE/87986aad70b2aa2fc7e7ae1b729e7f4963533261/src/assets/images/myTeamExampleRU.png
--------------------------------------------------------------------------------
/src/assets/images/teamActionsExampleEN.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rolling-scopes-school/RSS-Teams-FE/87986aad70b2aa2fc7e7ae1b729e7f4963533261/src/assets/images/teamActionsExampleEN.png
--------------------------------------------------------------------------------
/src/assets/images/teamActionsExampleRU.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rolling-scopes-school/RSS-Teams-FE/87986aad70b2aa2fc7e7ae1b729e7f4963533261/src/assets/images/teamActionsExampleRU.png
--------------------------------------------------------------------------------
/src/assets/svg/check.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/svg/chevron-arrow.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/svg/copy.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/assets/svg/copy2clip.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/assets/svg/coursesSelectArrow.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/svg/cross-small.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/assets/svg/cross.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/assets/svg/edit.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/svg/filterIcon.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/svg/headerActiveElement.svg:
--------------------------------------------------------------------------------
1 |
5 |
6 |
--------------------------------------------------------------------------------
/src/assets/svg/info.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/assets/svg/menuToggle.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/assets/svg/paginateArrowLeft.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/svg/paginateArrowRight.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/svg/plus.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/assets/svg/team-header-background-pattern.svg:
--------------------------------------------------------------------------------
1 |
16 |
--------------------------------------------------------------------------------
/src/assets/svg/team-header-decorations.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/components/App/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const AppStyled = styled.div`
4 | position: relative;
5 | height: 100vh;
6 | min-height: 100vh;
7 | display: flex;
8 | flex-direction: column;
9 | align-items: center;
10 | `;
11 |
--------------------------------------------------------------------------------
/src/components/CourseField/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, SelectHTMLAttributes } from 'react';
2 | import { Label, Select, SelectInner } from 'typography';
3 | import { FieldWrapper, SelectCourse } from './styled';
4 | import { ValidationAlert } from '../InputField/styled';
5 | import { useTranslation } from 'react-i18next';
6 |
7 | type Course = {
8 | id: string;
9 | name: string;
10 | };
11 |
12 | interface SelectFieldProps extends SelectHTMLAttributes {
13 | labelText?: string;
14 | placeholder: string;
15 | multi?: boolean;
16 | register: any;
17 | courses: Course[];
18 | onAdd?: any;
19 | isValid?: boolean;
20 | }
21 |
22 | export const CourseField: FC = ({
23 | labelText,
24 | placeholder,
25 | register,
26 | courses,
27 | onAdd,
28 | isValid,
29 | ...rest
30 | }) => {
31 | const { t } = useTranslation();
32 | const courseOptions = courses
33 | ? courses.map((course: Course) => {
34 | return (
35 |
38 | );
39 | })
40 | : null;
41 | return (
42 |
43 | {labelText && }
44 |
45 |
61 |
62 | {!isValid && {t('You need to choose at least one course')}}
63 |
64 | );
65 | };
66 |
--------------------------------------------------------------------------------
/src/components/CourseField/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { BG_COLOR, LIGHT_TEXT_COLOR, MAIN1_COLOR, WHITE_COLOR } from 'appConstants/colors';
3 | import { SVGParamsAdaptive } from 'typography';
4 |
5 | type TPlusButton = {
6 | active?: boolean;
7 | };
8 |
9 | export const FieldWrapper = styled.div`
10 | display: flex;
11 | flex-direction: column;
12 | margin-bottom: 20px;
13 | `;
14 |
15 | export const SelectCourse = styled.div`
16 | width: 300px;
17 | margin-bottom: 0;
18 | display: flex;
19 | flex-direction: row;
20 | justify-content: space-between;
21 | @media (max-width: 440px) {
22 | width: 100%;
23 | }
24 | `;
25 |
26 | export const CourseButton = styled.button`
27 | width: 40px;
28 | height: 40px;
29 | margin-left: 10px;
30 | padding: 10px;
31 | outline: none;
32 | border-radius: 10px;
33 | border: none;
34 | cursor: pointer;
35 |
36 | svg {
37 | ${SVGParamsAdaptive};
38 | }
39 | `;
40 |
41 | export const PlusButton = styled(CourseButton)`
42 | background-color: ${({ active }) => (active ? MAIN1_COLOR : BG_COLOR)};
43 |
44 | path {
45 | stroke: ${({ active }) => (active ? WHITE_COLOR : LIGHT_TEXT_COLOR)};
46 | }
47 | `;
48 |
49 | export const CrossButton = styled(CourseButton)`
50 | background: ${BG_COLOR}
51 | url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M3.05029 3.05054L12.9499 12.9501' stroke='%237E96C2' stroke-width='2' stroke-linecap='round'/%3E%3Cpath d='M12.9497 3.05029L3.0501 12.9499' stroke='%237E96C2' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E%0A")
52 | no-repeat center center;
53 | `;
54 |
55 | export const PlaceholderOption = styled.option`
56 | display: none;
57 | `;
58 |
--------------------------------------------------------------------------------
/src/components/ErrorBoundary/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { BoundryContainer } from './styled';
3 |
4 | interface ErrorBoundaryState {
5 | error: any;
6 | errorInfo: any;
7 | }
8 |
9 | class ErrorBoundary extends Component {
10 | state = { error: null, errorInfo: null };
11 |
12 | static getDerivedStateFromError(error: any, errorInfo: any) {
13 | return { error: error, errorInfo: errorInfo };
14 | }
15 |
16 | componentDidCatch(error: any, errorInfo: any) {
17 | // Catch errors in any components below and re-render with error message
18 | this.setState({
19 | error: error,
20 | errorInfo: errorInfo,
21 | });
22 | // You can also log error messages to an error reporting service here
23 | }
24 |
25 | handleReload() {
26 | location.reload();
27 | }
28 |
29 | render() {
30 | const { children } = this.props;
31 |
32 | if (this.state.errorInfo) {
33 | return (
34 |
35 | Something went wrong.
36 |
Click on the page to reload it.
37 |
38 | );
39 | }
40 |
41 | return <>{children}>;
42 | }
43 | }
44 |
45 | export default ErrorBoundary;
46 |
--------------------------------------------------------------------------------
/src/components/ErrorBoundary/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { PageTitle } from 'typography';
3 |
4 | export const BoundryContainer = styled(PageTitle)`
5 | width: 100vw;
6 | height: 100vh;
7 | display: flex;
8 | justify-content: center;
9 | align-items: center;
10 | font-size: 30px;
11 | text-align: center;
12 | `;
13 |
--------------------------------------------------------------------------------
/src/components/ErrorModal/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 | import { Modal } from 'components';
3 | import { ApolloError } from '@apollo/client';
4 | import { UNAUTHORIZED_ERROR_MESSAGE } from 'appConstants';
5 |
6 | type Props = {
7 | title?: string;
8 | text?: string;
9 | text2?: string;
10 | open?: boolean;
11 | cancelText?: string;
12 | isCrossIconVisible?: boolean;
13 | error?: ApolloError;
14 | };
15 |
16 | export const ErrorModal: FC = ({
17 | title = 'Something went wrong!',
18 | text = 'Please, try again later.',
19 | text2 = '@besovadevka or @MadaShindeInai',
20 | open = true,
21 | isCrossIconVisible = false,
22 | cancelText = 'Ok',
23 | error,
24 | }) => {
25 | const isUserUnauthorized = !!error?.graphQLErrors.find(
26 | ({ message }) => message === UNAUTHORIZED_ERROR_MESSAGE
27 | );
28 |
29 | if (isUserUnauthorized) {
30 | return null;
31 | }
32 |
33 | const onClose = () => {
34 | location.reload();
35 | };
36 |
37 | return (
38 |
43 | );
44 | };
45 |
--------------------------------------------------------------------------------
/src/components/FilterForm/filterFormFields.ts:
--------------------------------------------------------------------------------
1 | import { TFilterForm } from 'types';
2 | import { InputFieldProps } from '../../components/InputField';
3 |
4 | export const filterFormFields: InputFieldProps[] = [
5 | {
6 | name: 'discord',
7 | labelText: 'Discord',
8 | placeholder: 'Enter discord name',
9 | register: {
10 | pattern: {
11 | value: /^[A-Za-z0-9@#-_() ]+$/i,
12 | message: 'This input is letters and digits only.',
13 | },
14 | maxLength: {
15 | value: 30,
16 | message: 'This input exceed maxLength.',
17 | },
18 | },
19 | },
20 | {
21 | name: 'github',
22 | labelText: 'GitHub',
23 | placeholder: 'Enter github name',
24 | register: {
25 | pattern: {
26 | value: /^[A-Za-z0-9-_ ]+$/i,
27 | message: 'This input is letters and digits only.',
28 | },
29 | maxLength: {
30 | value: 30,
31 | message: 'This input exceed maxLength.',
32 | },
33 | },
34 | },
35 | {
36 | name: 'location',
37 | labelText: 'Location',
38 | placeholder: 'Enter location',
39 | register: {
40 | pattern: {
41 | value: /^[A-Za-z\- ]+$/i,
42 | message: 'This input is letters only.',
43 | },
44 | maxLength: {
45 | value: 30,
46 | message: 'This input exceed maxLength.',
47 | },
48 | },
49 | },
50 | {
51 | name: 'courseName',
52 | labelText: 'Course',
53 | placeholder: 'Enter course name',
54 | register: {
55 | pattern: {
56 | value: /^[A-Za-z\- ]+$/i,
57 | message: 'This input is letters only.',
58 | },
59 | maxLength: {
60 | value: 30,
61 | message: 'This input exceed maxLength.',
62 | },
63 | },
64 | },
65 | ];
66 |
67 | export const filterSelectFields: [string, [string, string | boolean][], string, string][] = [
68 | [
69 | 'Sort by score',
70 | [
71 | ['Max score', 'DESC'],
72 | ['Min score', 'ASC'],
73 | ],
74 | '100%',
75 | 'sortingOrder',
76 | ],
77 | [
78 | 'Sort by team',
79 | [
80 | ['All', false],
81 | ['Without team', true],
82 | ],
83 | '100%',
84 | 'teamFilter',
85 | ],
86 | ];
87 |
88 | export const defaultFilterData: TFilterForm = {
89 | discord: null,
90 | github: null,
91 | location: null,
92 | courseName: null,
93 | sortingOrder: filterSelectFields[0][1][0][0],
94 | teamFilter: filterSelectFields[1][1][0][0],
95 | };
96 |
--------------------------------------------------------------------------------
/src/components/FilterForm/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { MAIN1_COLOR } from 'appConstants/colors';
3 | import { EditProfileWrapper } from 'modules/EditProfile/styled';
4 | import { Button } from 'typography';
5 |
6 | type TFilerButtonProps = {
7 | bgColor?: string | undefined;
8 | clearBtn?: boolean;
9 | outerBtn?: boolean;
10 | };
11 |
12 | export const FilterFormBase = styled(EditProfileWrapper)`
13 | z-index: 2;
14 | position: absolute;
15 | top: 25px;
16 | right: 0;
17 | display: flex;
18 | flex-direction: column;
19 | justify-content: space-between;
20 | height: 412px;
21 | @media screen and (max-width: 768px) {
22 | height: auto;
23 | }
24 | @media (max-width: 440px) {
25 | top: 5px;
26 | right: -13px;
27 | width: 320px;
28 | }
29 | `;
30 |
31 | export const FilterButton = styled(Button)`
32 | background-color: ${({ bgColor }) => bgColor ?? 'transparent'};
33 | color: ${MAIN1_COLOR};
34 | display: flex;
35 | align-items: center;
36 | gap: ${({ clearBtn = false }) => (clearBtn ? '11px' : '20px')};
37 | margin-left: ${({ outerBtn = false }) => (outerBtn ? 'auto' : '0')};
38 | padding: ${({ clearBtn = false }) => (clearBtn ? '13px 20px 13px 0' : '20px 30px')};
39 |
40 | @media (max-width: 1200px) {
41 | padding: ${({ clearBtn = false }) => (clearBtn ? '12px 16px 12px 0' : '18px 28px')};
42 | gap: ${({ clearBtn = false }) => (clearBtn ? '10px' : '18px')};
43 | }
44 | @media (max-width: 992px) {
45 | padding: ${({ clearBtn = false }) => (clearBtn ? '13px 12px 13px 0' : '14px 26px')};
46 | gap: ${({ clearBtn = false }) => (clearBtn ? '10px' : '17px')};
47 | }
48 | @media (max-width: 768px) {
49 | padding: ${({ clearBtn = false }) => (clearBtn ? '13px 8px 13px 0' : '8px 26px')};
50 | gap: ${({ clearBtn = false }) => (clearBtn ? '10px' : '16px')};
51 | }
52 | @media (max-width: 550px) {
53 | padding: ${({ clearBtn = false }) => (clearBtn ? '11px 5px 11px 0' : '5px 25px')};
54 | gap: ${({ clearBtn = false }) => (clearBtn ? '8px' : '13px')};
55 | }
56 | @media (max-width: 440px) {
57 | padding: ${({ clearBtn = false }) => (clearBtn ? '9px 5px 9px 0' : '5px 20px')};
58 | gap: ${({ clearBtn = false }) => (clearBtn ? '6px' : '10px')};
59 | }
60 |
61 | img {
62 | filter: invert(100%) sepia() saturate(10000%) hue-rotate(-110deg);
63 |
64 | @media (max-width: 992px) {
65 | width: 15px;
66 | }
67 | @media (max-width: 768px) {
68 | width: 14px;
69 | }
70 | @media (max-width: 550px) {
71 | width: 12px;
72 | }
73 | @media (max-width: 440px) {
74 | width: 10px;
75 | }
76 | }
77 | `;
78 |
79 | export const FilterButtonsWrapper = styled.div`
80 | width: 100%;
81 | display: flex;
82 | align-items: center;
83 |
84 | .SecondButtonForm {
85 | margin-left: auto;
86 | }
87 | `;
88 |
--------------------------------------------------------------------------------
/src/components/FilterSelect/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, SelectHTMLAttributes } from 'react';
2 | import { Label, Select, SelectInner } from 'typography';
3 | import { FieldWrapper, SelectCourse } from 'components/CourseField/styled';
4 | import { useTranslation } from 'react-i18next';
5 | interface SelectFieldProps extends SelectHTMLAttributes {
6 | labelText?: string;
7 | placeholder: string;
8 | register: any;
9 | options: string[];
10 | currentOption: string;
11 | }
12 |
13 | export const FilterSelect: FC = ({
14 | labelText,
15 | placeholder,
16 | register,
17 | options,
18 | currentOption,
19 | ...rest
20 | }) => {
21 | const { t } = useTranslation();
22 | const filterFieldOptions =
23 | options.map((option: string) => {
24 | return (
25 |
28 | );
29 | }) ?? null;
30 | return (
31 |
32 | {labelText && }
33 |
34 |
39 |
40 |
41 | );
42 | };
43 |
--------------------------------------------------------------------------------
/src/components/Footer/components/FooterContent/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 | import { FooterContentBlock, FooterContentWrapper, FooterTitle } from 'components/Footer/styled';
3 | import { FOOTER_INFO, LINK_TO_DESIGN_BLOCK } from 'appConstants';
4 | import { useTranslation } from 'react-i18next';
5 |
6 | export const FooterContent: FC = () => {
7 | const { t } = useTranslation();
8 | return (
9 |
10 | {FOOTER_INFO.map((item, index) => {
11 | return (
12 |
13 | {t(item.title)}
14 |
15 | {item.members.map((item: string) => {
16 | return (
17 |
24 | {item}
25 |
26 | );
27 | })}
28 |
29 |
30 | );
31 | })}
32 |
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/src/components/Footer/components/index.ts:
--------------------------------------------------------------------------------
1 | export { FooterContent } from './FooterContent';
2 |
--------------------------------------------------------------------------------
/src/components/Footer/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 | import { RSLogo } from 'typography';
3 | import { StyledFooter, FooterWrapper, FooterContentBlockLogo } from './styled';
4 | import { Link } from 'react-router-dom';
5 | import { FooterContent } from './components';
6 |
7 | export const Footer: FC = () => {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | );
20 | };
21 |
--------------------------------------------------------------------------------
/src/components/Footer/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { DARK_TEXT_COLOR, FOOTER_NAMES_COLOR, WHITE_COLOR } from 'appConstants/colors';
3 | import { GeneralAdaptiveFont } from 'typography';
4 |
5 | export const StyledFooter = styled.footer`
6 | position: sticky;
7 | top: 100%;
8 | z-index: 2;
9 | display: flex;
10 | justify-content: center;
11 | align-items: center;
12 | width: 100%;
13 | height: 110px;
14 | padding: 0 4.2%;
15 | background-color: ${DARK_TEXT_COLOR};
16 |
17 | @media (max-width: 992px) {
18 | height: 100px;
19 | }
20 | @media (max-width: 880px) {
21 | height: 80px;
22 | }
23 | @media (max-width: 768px) {
24 | height: 65px;
25 | }
26 | @media (max-width: 550px) {
27 | height: 60px;
28 | }
29 | @media (max-width: 440px) {
30 | height: 50px;
31 | }
32 | `;
33 |
34 | export const FooterWrapper = styled.div`
35 | display: flex;
36 | justify-content: space-between;
37 | width: 100%;
38 | max-width: 1320px;
39 | height: 100%;
40 | `;
41 |
42 | export const FooterContentWrapper = styled.div`
43 | display: flex;
44 | justify-content: flex-end;
45 | width: 100%;
46 | gap: 5.6%;
47 |
48 | @media (max-width: 880px) {
49 | display: none;
50 | }
51 | `;
52 |
53 | export const FooterContentBlock = styled.div`
54 | display: flex;
55 | flex-direction: column;
56 | justify-content: center;
57 | gap: 13px;
58 |
59 | .contentBlock {
60 | display: flex;
61 | flex-wrap: wrap;
62 | min-height: 24px;
63 | gap: 30px;
64 |
65 | @media (max-width: 992px) {
66 | gap: 20px;
67 | }
68 |
69 | .contentItem {
70 | height: 100%;
71 | margin-bottom: -20px;
72 | font: 400 1rem/24px 'Poppins', sans-serif;
73 | color: ${FOOTER_NAMES_COLOR};
74 | text-decoration: none;
75 | outline: none;
76 | ${GeneralAdaptiveFont};
77 |
78 | &:hover {
79 | color: ${WHITE_COLOR};
80 | }
81 |
82 | @media (max-width: 992px) {
83 | margin-bottom: 15px;
84 | }
85 | }
86 | }
87 |
88 | .contentBlock.designBlock {
89 | width: auto;
90 | }
91 |
92 | .contentItem.designItem {
93 | width: 140px;
94 | }
95 | `;
96 |
97 | export const FooterContentBlockLogo = styled.div`
98 | display: flex;
99 | justify-content: space-between;
100 | align-items: center;
101 |
102 | a {
103 | height: 100%;
104 | display: flex;
105 | align-items: center;
106 | }
107 | `;
108 |
109 | export const FooterTitle = styled.h1`
110 | font: 600 1rem/24px 'Poppins', sans-serif;
111 | color: ${WHITE_COLOR};
112 | margin: 0;
113 | ${GeneralAdaptiveFont};
114 | `;
115 |
--------------------------------------------------------------------------------
/src/components/Header/components/BurgerMenu/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 | import { useDispatch, useSelector } from 'react-redux';
3 | import { NavLink } from 'react-router-dom';
4 | import { selectIsBurgerMenuOpen } from 'modules/LoginPage/selectors';
5 | import {
6 | BurgerMenuWrapper,
7 | BurgerMenuLayout,
8 | CrossButton,
9 | BurgerMenuNavList,
10 | BurgerMenuNavListItem,
11 | BurgerMenuOverlay,
12 | } from './styled';
13 | import { setBurgerMenuOpen } from 'modules/LoginPage/loginPageReducer';
14 | import { APP_NAVIGATION_LINKS } from 'appConstants';
15 | import { useTranslation } from 'react-i18next';
16 | import { TNavLink } from 'types';
17 | import { LangSelect } from '../MenuWrapper/components/LangSelect';
18 |
19 | type BurgerMenuProps = {
20 | newUserCheck: boolean;
21 | navOnClickHandler: (e: React.MouseEvent, path: string) => void;
22 | };
23 |
24 | export const BurgerMenu: FC = ({ newUserCheck, navOnClickHandler }) => {
25 | const dispatch = useDispatch();
26 | const { t } = useTranslation();
27 | const isBurgerMenuOpen = useSelector(selectIsBurgerMenuOpen);
28 |
29 | const onClickMenuToggle = () => {
30 | dispatch(setBurgerMenuOpen(!isBurgerMenuOpen));
31 | };
32 |
33 | return (
34 |
35 |
36 |
37 |
38 |
39 | {Object.values(APP_NAVIGATION_LINKS).map((link: TNavLink, index: number) => {
40 | if (+newUserCheck + +link.isAlwaysVisible) {
41 | return (
42 |
43 | {
48 | onClickMenuToggle();
49 | navOnClickHandler(e, Object.keys(APP_NAVIGATION_LINKS)[index]);
50 | }}
51 | >
52 | {t(link.name)}
53 |
54 |
55 | );
56 | }
57 | })}
58 |
59 |
60 |
61 |
62 | );
63 | };
64 |
--------------------------------------------------------------------------------
/src/components/Header/components/BurgerMenu/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { DARK_TEXT_COLOR, OVERLAY_COLOR, WHITE_COLOR } from 'appConstants/colors';
3 | import { ReactComponent as IconClose } from 'assets/svg/cross.svg';
4 |
5 | type BurgerMenuProps = {
6 | isBurgerMenuOpen: boolean;
7 | };
8 |
9 | export const BurgerMenuWrapper = styled.div`
10 | position: fixed;
11 | z-index: 3;
12 | top: 0;
13 | display: flex;
14 | width: 100vw;
15 | height: 100vh;
16 | transition: transform 0.6s ease-in-out;
17 | transform: ${({ isBurgerMenuOpen }) => (isBurgerMenuOpen ? 'translateX(0)' : 'translateX(100%)')};
18 | `;
19 |
20 | export const BurgerMenuOverlay = styled.div`
21 | width: calc(100% - 250px);
22 | height: 100%;
23 | background-color: ${({ isBurgerMenuOpen }) => (isBurgerMenuOpen ? OVERLAY_COLOR : 'none')};
24 | transition: background-color 0.6s ease-in-out;
25 |
26 | @media (max-width: 440px) {
27 | width: calc(100% - 180px);
28 | }
29 | `;
30 |
31 | export const BurgerMenuLayout = styled.nav`
32 | display: flex;
33 | flex-direction: column;
34 | gap: 40px;
35 | width: 250px;
36 | height: 100%;
37 | padding: 20px 20px 20px 40px;
38 | background: ${WHITE_COLOR};
39 |
40 | @media (max-width: 440px) {
41 | width: 180px;
42 | padding-left: 20px;
43 | }
44 | `;
45 |
46 | export const CrossButton = styled(IconClose)`
47 | width: 16px;
48 | height: 16px;
49 | align-self: flex-end;
50 | cursor: pointer;
51 | `;
52 |
53 | export const BurgerMenuNavList = styled.ul`
54 | display: flex;
55 | flex-direction: column;
56 | gap: 40px;
57 | margin: 0;
58 | padding-inline-start: 0;
59 | `;
60 |
61 | export const BurgerMenuNavListItem = styled.li`
62 | list-style-type: none;
63 |
64 | a {
65 | text-decoration: none;
66 | color: ${DARK_TEXT_COLOR};
67 | }
68 |
69 | a.activeNavLink,
70 | a:hover {
71 | font-weight: 700;
72 | }
73 | `;
74 |
--------------------------------------------------------------------------------
/src/components/Header/components/MenuWrapper/components/CoursesSelect/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, useState } from 'react';
2 | import { useDispatch, useSelector } from 'react-redux';
3 | import { selectCurrCourse } from 'modules/LoginPage/selectors';
4 | import { selectUserData } from 'modules/StudentsTable/selectors';
5 | import { Course } from 'types';
6 | import { CommonSelectList } from 'components';
7 | import { setCourse } from 'modules/LoginPage/loginPageMiddleware';
8 |
9 | export const CoursesSelect: FC = () => {
10 | const [isCourseSelectOpen, setCourseSelectOpen] = useState(false);
11 | const dispatch = useDispatch();
12 | const currCourse = useSelector(selectCurrCourse);
13 | const userData = useSelector(selectUserData);
14 |
15 | const userCourses = userData.courses.filter((item) => item.id !== currCourse.id) ?? null;
16 |
17 | const onCourseChange = (course: Course) => {
18 | setCourseSelectOpen(false);
19 | dispatch(setCourse(course));
20 | };
21 |
22 | return (
23 |
31 | );
32 | };
33 |
--------------------------------------------------------------------------------
/src/components/Header/components/MenuWrapper/components/LangSelect/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, useState } from 'react';
2 | import { useDispatch, useSelector } from 'react-redux';
3 | import { LANGUAGES } from 'appConstants';
4 | import { selectCurrLanguage } from 'modules/LoginPage/selectors';
5 | import i18n from 'translation/resources';
6 | import { CommonSelectList } from 'components';
7 | import { setLanguage } from 'modules/LoginPage/loginPageMiddleware';
8 |
9 | type LangSelectProps = {
10 | menuToggle?: boolean;
11 | customStyle?: boolean;
12 | };
13 |
14 | export const LangSelect: FC = ({ menuToggle, customStyle }) => {
15 | const [displayLangList, setDisplayLangList] = useState(false);
16 | const dispatch = useDispatch();
17 | const currentLanguage = useSelector(selectCurrLanguage);
18 |
19 | const onLangChange = (item: string) => {
20 | setDisplayLangList(false);
21 | dispatch(setLanguage(item));
22 | i18n.changeLanguage(item);
23 | };
24 |
25 | const languages: string[] = LANGUAGES.filter((lang) => lang !== currentLanguage);
26 |
27 | return (
28 |
37 | );
38 | };
39 |
--------------------------------------------------------------------------------
/src/components/Header/components/MenuWrapper/components/Nav/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 | import { useTranslation } from 'react-i18next';
3 | import { APP_NAVIGATION_LINKS } from 'appConstants';
4 | import { NavLink } from 'react-router-dom';
5 | import { StyledNav, StyledNavList, StyledNavListItem, StyledHeaderActiveElement } from './styled';
6 | import { TNavLink } from 'types';
7 |
8 | type NavProps = {
9 | newUserCheck: boolean;
10 | navOnClickHandler: (e: React.MouseEvent, path: string) => void;
11 | isUserAdmin: boolean;
12 | };
13 |
14 | export const Nav: FC = ({ newUserCheck, navOnClickHandler, isUserAdmin }) => {
15 | const { t } = useTranslation();
16 |
17 | return (
18 |
19 |
20 | {Object.values(APP_NAVIGATION_LINKS).map((link: TNavLink, index: number) => {
21 | const isNavLinkAvailable = !!(+newUserCheck + +link.isAlwaysVisible);
22 | if (isNavLinkAvailable) {
23 | if (link.name === 'Admin' && !isUserAdmin) return null;
24 | return (
25 |
26 | navOnClickHandler(e, Object.keys(APP_NAVIGATION_LINKS)[index])}
31 | >
32 | {t(link.name)}
33 |
34 |
35 |
36 | );
37 | }
38 | })}
39 |
40 |
41 | );
42 | };
43 |
--------------------------------------------------------------------------------
/src/components/Header/components/MenuWrapper/components/Nav/styled.ts:
--------------------------------------------------------------------------------
1 | import { WHITE_COLOR } from 'appConstants/colors';
2 | import styled, { keyframes } from 'styled-components';
3 | import { ReactComponent as HeaderActiveElement } from 'assets/svg/headerActiveElement.svg';
4 |
5 | type TStyledNavListItemProps = {
6 | newUserCheck: boolean;
7 | };
8 |
9 | const open = keyframes`
10 | from: { height: 0 }
11 | to: { height: 100%}
12 | `;
13 |
14 | export const StyledNav = styled.nav`
15 | display: flex;
16 | justify-content: center;
17 | align-items: center;
18 | align-self: flex-end;
19 | margin-right: 60px;
20 | font: 400 1rem/24px 'Poppins', sans-serif;
21 | `;
22 |
23 | export const StyledNavList = styled.ul`
24 | @media (max-width: 1430px) {
25 | display: none;
26 | }
27 |
28 | display: flex;
29 | justify-content: center;
30 | align-items: center;
31 | margin: 0;
32 | padding-inline-start: 0;
33 | gap: 60px;
34 | `;
35 |
36 | export const StyledHeaderActiveElement = styled(HeaderActiveElement)`
37 | width: 87px;
38 | height: 0;
39 | `;
40 |
41 | export const StyledNavListItem = styled.li`
42 | list-style-type: none;
43 |
44 | a {
45 | display: flex;
46 | flex-direction: column;
47 | justify-content: space-between;
48 | align-items: center;
49 | height: 52px;
50 | color: ${WHITE_COLOR};
51 | text-decoration: none;
52 |
53 | &:hover {
54 | font-weight: 700;
55 | }
56 |
57 | svg {
58 | height: 0;
59 | margin-bottom: -5px;
60 | transition: all 0.5s ease-in-out;
61 | animation: ${open} 0.5s ease-in-out;
62 | }
63 | }
64 |
65 | a.activeNavLink {
66 | font-weight: 700;
67 |
68 | svg {
69 | height: 22px;
70 | margin-bottom: 0;
71 | }
72 | }
73 | `;
74 |
--------------------------------------------------------------------------------
/src/components/Header/components/MenuWrapper/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 | import { useDispatch, useSelector } from 'react-redux';
3 | import { selectUserData } from 'modules/StudentsTable/selectors';
4 | import { CoursesSelect } from './components/CoursesSelect';
5 | import { Nav } from './components/Nav';
6 | import { MenuButton, StyledMenuWrapper } from './styled';
7 | import { LangSelect } from './components/LangSelect';
8 | import { setBurgerMenuOpen } from 'modules/LoginPage/loginPageReducer';
9 | import { selectIsBurgerMenuOpen } from 'modules/LoginPage/selectors';
10 |
11 | type MenuWrapperProps = {
12 | navOnClickHandler: (e: React.MouseEvent, path: string) => void;
13 | };
14 |
15 | export const MenuWrapper: FC = ({ navOnClickHandler }) => {
16 | const dispatch = useDispatch();
17 | const userData = useSelector(selectUserData);
18 | const isBurgerMenuOpen = useSelector(selectIsBurgerMenuOpen);
19 |
20 | const newUserCheck = !!userData?.courses.length;
21 | const isUserAdmin = !!userData?.isAdmin;
22 |
23 | const onClickMenuToggle = () => {
24 | dispatch(setBurgerMenuOpen(!isBurgerMenuOpen));
25 | };
26 |
27 | return (
28 |
29 |
30 | {newUserCheck && }
31 |
32 |
33 |
34 | );
35 | };
36 |
--------------------------------------------------------------------------------
/src/components/Header/components/MenuWrapper/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { ReactComponent as MenuToggle } from 'assets/svg/menuToggle.svg';
3 |
4 | export const StyledMenuWrapper = styled.div`
5 | display: flex;
6 | height: 60px;
7 |
8 | @media (max-width: 1260px) {
9 | height: 40px;
10 | }
11 | @media (max-width: 550px) {
12 | margin-left: -30px;
13 | }
14 | @media (max-width: 440px) {
15 | margin-left: -50px;
16 | }
17 | `;
18 |
19 | export const MenuButton = styled(MenuToggle)`
20 | width: 0;
21 | height: 0;
22 | cursor: pointer;
23 |
24 | @media (max-width: 1430px) {
25 | width: 24px;
26 | height: 24px;
27 | margin: 8px 0 0 40px;
28 | }
29 | @media (max-width: 700px) {
30 | width: 20px;
31 | height: 20px;
32 | margin: 10px 0 0 30px;
33 | }
34 | @media (max-width: 600px) {
35 | margin-left: 0;
36 | }
37 | @media (max-width: 440px) {
38 | width: 16px;
39 | height: 16px;
40 | margin-top: 12px;
41 | }
42 | `;
43 |
--------------------------------------------------------------------------------
/src/components/Header/components/index.ts:
--------------------------------------------------------------------------------
1 | export { MenuWrapper } from './MenuWrapper';
2 | export { BurgerMenu } from './BurgerMenu';
3 |
--------------------------------------------------------------------------------
/src/components/Header/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 | import { Link } from 'react-router-dom';
3 | import { useDispatch, useSelector } from 'react-redux';
4 | import { RSLogo } from 'typography';
5 | import { StyledHeader, Container } from './styled';
6 | import { selectIsEditProfileDataChange, selectToken } from 'modules/LoginPage/selectors';
7 | import { MenuWrapper, BurgerMenu } from './components';
8 | import { selectUserData } from 'modules/StudentsTable/selectors';
9 | import { activeModalLeavePage } from 'modules/TeamsList/teamsListReducer';
10 | import { setPathToThePage } from 'modules/LoginPage/loginPageReducer';
11 |
12 | export const Header: FC = () => {
13 | const dispatch = useDispatch();
14 | const userData = useSelector(selectUserData);
15 | const loginToken = useSelector(selectToken);
16 | const isEditProfileDataChange = useSelector(selectIsEditProfileDataChange);
17 | const newUserCheck = !!userData?.courses.length;
18 |
19 | const navOnClickHandler = (e: React.MouseEvent, path: string) => {
20 | if (isEditProfileDataChange) {
21 | e.preventDefault();
22 | dispatch(activeModalLeavePage(true));
23 | dispatch(setPathToThePage(path));
24 | }
25 | };
26 |
27 | return (
28 | <>
29 |
30 |
31 |
32 |
33 |
34 | {loginToken && }
35 |
36 |
37 |
38 | >
39 | );
40 | };
41 |
--------------------------------------------------------------------------------
/src/components/Header/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { MAIN1_COLOR } from 'appConstants/colors';
3 | import { GeneralAdaptiveFont } from 'typography';
4 |
5 | type TStyledHeaderProps = {
6 | login: string | null;
7 | };
8 |
9 | export const StyledHeader = styled.header`
10 | position: sticky;
11 | display: flex;
12 | justify-content: center;
13 | align-items: flex-end;
14 | height: 80px;
15 | padding: ${({ login }) => (login ? '1.4% 4.2% 0' : '2.8% 4.2% 0 2.8%')};
16 | background-color: ${({ login }) => (login ? MAIN1_COLOR : 'transparent')};
17 | width: 100%;
18 | z-index: 1;
19 |
20 | @media (max-width: 1260px) {
21 | align-items: center;
22 | }
23 | `;
24 |
25 | export const Container = styled.div`
26 | display: flex;
27 | justify-content: space-between;
28 | width: 100%;
29 | max-width: 1320px;
30 | ${GeneralAdaptiveFont};
31 | `;
32 |
--------------------------------------------------------------------------------
/src/components/InputField/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, InputHTMLAttributes } from 'react';
2 | import { Input } from 'typography';
3 | import { FieldWrapper, ValidationAlert, FLabel } from './styled';
4 | import { useTranslation } from 'react-i18next';
5 | export interface InputFieldProps extends InputHTMLAttributes {
6 | labelText?: string;
7 | placeholder?: string;
8 | register?: any;
9 | name: string;
10 | message?: string | undefined;
11 | onChange?: (e: React.ChangeEvent) => void;
12 | }
13 |
14 | export const InputField: FC = ({
15 | labelText,
16 | placeholder,
17 | register,
18 | name,
19 | message,
20 | onChange,
21 | }) => {
22 | const { t } = useTranslation();
23 | return (
24 |
25 | {labelText ? t(labelText) : ''}
26 |
34 | {message ? t(message) : ''}
35 |
36 | );
37 | };
38 |
--------------------------------------------------------------------------------
/src/components/InputField/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { ALERT_COLOR } from 'appConstants/colors';
3 | import { GeneralAdaptiveFont, Label } from 'typography';
4 |
5 | export const FieldWrapper = styled.div`
6 | display: flex;
7 | flex-direction: column;
8 | margin-bottom: 0;
9 | `;
10 |
11 | export const ValidationAlert = styled.div`
12 | ${GeneralAdaptiveFont}
13 | color: ${ALERT_COLOR};
14 | font-size: 14px;
15 | height: 22px;
16 | `;
17 |
18 | export const FLabel = styled(Label)`
19 | margin-bottom: 8px;
20 | `;
21 |
--------------------------------------------------------------------------------
/src/components/Loader/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 | import { LoaderStyled, LoaderWrapper } from './styled';
3 |
4 | export const Loader: FC = () => {
5 | return (
6 |
7 |
8 |
13 |
14 |
15 | );
16 | };
17 |
--------------------------------------------------------------------------------
/src/components/Loader/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { MAIN1_COLOR } from 'appConstants/colors';
3 | import { MainComponentHeight } from 'typography';
4 |
5 | export const LoaderWrapper = styled.div`
6 | position: absolute;
7 | top: 50%;
8 | left: 50%;
9 | width: 100%;
10 | display: flex;
11 | justify-content: center;
12 | align-items: center;
13 | transform: translate(-50%, -50%);
14 | ${MainComponentHeight};
15 | `;
16 |
17 | export const LoaderStyled = styled.div`
18 | width: 25%;
19 | .loader {
20 | display: flex;
21 | align-items: center;
22 | justify-content: center;
23 | width: 100%;
24 | height: 2.5em;
25 | gap: 12px;
26 | animation-duration: 1.4s;
27 | animation-delay: 0.32s;
28 | margin: auto;
29 | text-align: center;
30 |
31 | @media (max-width: 440px) {
32 | gap: 9px;
33 | }
34 |
35 | .loader-child {
36 | width: 1.8em;
37 | height: 1.8em;
38 | background-color: ${MAIN1_COLOR};
39 |
40 | border-radius: 100%;
41 | display: inline-block;
42 | animation: loader-in 1.6s ease-in-out 0s infinite both;
43 |
44 | @media (max-width: 1200px) {
45 | width: 1.7em;
46 | height: 1.7em;
47 | }
48 | @media (max-width: 550px) {
49 | width: 1.4em;
50 | height: 1.4em;
51 | }
52 | @media (max-width: 440px) {
53 | width: 1em;
54 | height: 1em;
55 | }
56 | }
57 |
58 | .loader-child-1 {
59 | animation-delay: -0.6s;
60 | }
61 | .loader-child-2 {
62 | animation-delay: -0.4s;
63 | }
64 | .loader-child-3 {
65 | animation-delay: -0.2s;
66 | }
67 | }
68 |
69 | @keyframes loader-in {
70 | 0%,
71 | 80%,
72 | 100% {
73 | transform: scale(0);
74 | }
75 | 40% {
76 | transform: scale(1);
77 | }
78 | }
79 | `;
80 |
--------------------------------------------------------------------------------
/src/components/Modal/index.module.css:
--------------------------------------------------------------------------------
1 | /* setting absolute can break other modals */
2 | .overlay {
3 | position: fixed;
4 | top: 0;
5 | right: 0;
6 | bottom: 0;
7 | left: 0;
8 | z-index: 999;
9 | text-align: center;
10 | background: var(--OVERLAY_COLOR);
11 |
12 | overscroll-behavior: contain;
13 | }
14 |
15 | /* setting max width to container can break other modals */
16 | .container {
17 | position: relative;
18 | display: flex;
19 | flex-direction: column;
20 | width: 440px;
21 | margin: 11vh auto;
22 | padding: 30px 40px;
23 | text-align: center;
24 | background: var(--WHITE_COLOR);
25 | border: none;
26 | border-radius: 20px;
27 | }
28 |
29 | .icon {
30 | position: absolute;
31 | top: 20px;
32 | right: 20px;
33 | z-index: 1;
34 | width: 25px;
35 | height: 25px;
36 | cursor: pointer;
37 | }
38 |
39 | @media (max-width: 550px) {
40 | .container {
41 | width: 350px;
42 | }
43 |
44 | .icon {
45 | top: 15px;
46 | right: 20px;
47 | width: 22px;
48 | height: 22px;
49 | }
50 | }
51 |
52 | @media (max-width: 440px) {
53 | .container {
54 | width: 300px;
55 | }
56 |
57 | .icon {
58 | top: 15px;
59 | right: 20px;
60 | width: 20px;
61 | height: 20px;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/components/ModalCreateEditTeam/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, useState, useEffect, useCallback } from 'react';
2 | import { useDispatch } from 'react-redux';
3 | import { Modal } from 'components';
4 | import { ModalInput } from 'typography';
5 | import { ValidationAlert } from '../InputField/styled';
6 | import { useTranslation } from 'react-i18next';
7 | import { setSocialLink } from 'modules/TeamsList/teamsListReducer';
8 | import { isFieldValid } from 'utils/isFieldValid';
9 |
10 | type Props = {
11 | title: string;
12 | text: string;
13 | open: boolean;
14 | onSubmit?: () => void;
15 | onClose: () => void;
16 | value: string;
17 | okText?: string;
18 | validateRules?: any;
19 | } & typeof defaultProps;
20 |
21 | const defaultProps = {
22 | open: false,
23 | okText: 'Create team',
24 | };
25 |
26 | export const ModalCreateEditTeam: FC = ({
27 | title,
28 | text,
29 | open,
30 | okText,
31 | value,
32 | onClose,
33 | onSubmit,
34 | validateRules,
35 | }) => {
36 | const dispatch = useDispatch();
37 | const { t } = useTranslation();
38 | const [isInputValid, setInputValid] = useState(false);
39 | const [errorMessage, setErrorMessage] = useState('');
40 |
41 | const onSubmitModal = useCallback(() => {
42 | if (isInputValid && onSubmit) {
43 | onSubmit();
44 | onClose();
45 | } else {
46 | setErrorMessage('Please, enter link');
47 | }
48 | }, [onClose, onSubmit, isInputValid]);
49 |
50 | const onChangeModal = (e: React.ChangeEvent) => {
51 | dispatch(setSocialLink(e.target.value.trim()));
52 | isFieldValid(
53 | e.target.value.trim(),
54 | validateRules,
55 | !!validateRules,
56 | setInputValid,
57 | setErrorMessage
58 | );
59 | };
60 |
61 | const onCloseModal = () => {
62 | onClose();
63 | setErrorMessage('');
64 | };
65 |
66 | useEffect(() => {
67 | const listener = (e: KeyboardEvent) => {
68 | if (e.key === 'Enter') {
69 | onSubmitModal();
70 | }
71 | };
72 | if (open) {
73 | document.addEventListener('keydown', listener);
74 | }
75 | return () => document.removeEventListener('keydown', listener);
76 | }, [onSubmitModal, open]);
77 |
78 | return (
79 |
86 |
94 | {!isInputValid && {t(errorMessage)}}
95 |
96 | );
97 | };
98 |
--------------------------------------------------------------------------------
/src/components/ModalCreated/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, useState } from 'react';
2 | import styled from 'styled-components';
3 | import { Modal } from 'components';
4 | import { ModalInput, InvertedButton } from 'typography';
5 | import { BG_COLOR } from 'appConstants/colors';
6 | import CopyIcon from 'assets/svg/copy2clip.svg';
7 |
8 | const InputWithCopy = styled.div`
9 | position: relative;
10 | `;
11 |
12 | const CopyButton = styled(InvertedButton)`
13 | right: 40px;
14 | bottom: 0;
15 | padding: 19px 15px;
16 | position: absolute;
17 | background: ${BG_COLOR} url(${CopyIcon}) no-repeat center center;
18 | `;
19 |
20 | type Props = {
21 | title: string;
22 | text: string;
23 | text2?: string;
24 | open: boolean;
25 | onClose: () => void;
26 | cancelText?: string;
27 | password: string;
28 | } & typeof defaultProps;
29 |
30 | const defaultProps = {
31 | text2: '',
32 | };
33 |
34 | export const ModalCreated: FC = ({
35 | title,
36 | text,
37 | text2,
38 | open,
39 | cancelText,
40 | onClose,
41 | password,
42 | }) => {
43 | const [isCopy, setIsCopy] = useState(false);
44 |
45 | const CopyToClipboard = (text: string) => {
46 | navigator.clipboard
47 | .writeText(text)
48 | .then(() => {
49 | setIsCopy(true);
50 | setTimeout(() => {
51 | setIsCopy(false);
52 | }, 1000);
53 | })
54 | .catch((err) => {
55 | console.log('Something went wrong', err);
56 | });
57 | };
58 | const onClickCopyButton = () => CopyToClipboard(password);
59 |
60 | return (
61 |
62 |
63 |
64 |
65 |
66 |
67 | );
68 | };
69 |
--------------------------------------------------------------------------------
/src/components/ModalExpel/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, useEffect, useCallback } from 'react';
2 | import { Modal } from 'components';
3 |
4 | type Props = {
5 | title: string;
6 | text: string;
7 | open: boolean;
8 | onSubmit?: () => void;
9 | onClose: () => void;
10 | okText?: string;
11 | isCrossIconVisible?: boolean;
12 | cancelText?: string;
13 | };
14 |
15 | export const ModalExpel: FC = ({
16 | title,
17 | text,
18 | open,
19 | okText,
20 | cancelText,
21 | isCrossIconVisible = true,
22 | onClose,
23 | onSubmit,
24 | }) => {
25 | const onSubmitModal = useCallback(() => {
26 | onClose();
27 | if (onSubmit) {
28 | onSubmit();
29 | }
30 | }, [onClose, onSubmit]);
31 |
32 | useEffect(() => {
33 | const listener = (e: KeyboardEvent) => {
34 | if (e.key === 'Enter') {
35 | onSubmitModal();
36 | }
37 | };
38 | if (open) {
39 | document.addEventListener('keydown', listener);
40 | }
41 | return () => document.removeEventListener('keydown', listener);
42 | }, [onSubmitModal, open]);
43 |
44 | return (
45 |
59 | );
60 | };
61 |
--------------------------------------------------------------------------------
/src/components/ModalJoin/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, useCallback, useEffect } from 'react';
2 | import { useDispatch } from 'react-redux';
3 | import { Modal } from 'components';
4 | import { ModalInput } from 'typography';
5 | import { useTranslation } from 'react-i18next';
6 | import { setTeamPassword } from 'modules/TeamsList/teamsListReducer';
7 |
8 | type Props = {
9 | title: string;
10 | text: string;
11 | open: boolean;
12 | okText?: string;
13 | value: string;
14 | cancelText?: string;
15 | onClose: () => void;
16 | onSubmit?: (e: string) => void;
17 | onChange?: () => void;
18 | };
19 |
20 | export const ModalJoin: FC = ({
21 | title,
22 | text,
23 | open,
24 | okText,
25 | value,
26 | cancelText,
27 | onClose,
28 | onSubmit,
29 | onChange,
30 | }) => {
31 | const dispatch = useDispatch();
32 | const { t } = useTranslation();
33 |
34 | const onSubmitModal = useCallback(() => {
35 | if (onSubmit && value) {
36 | onSubmit(value);
37 | }
38 | }, [value, onSubmit]);
39 |
40 | const onChangeModal = (e: React.ChangeEvent) => {
41 | if (onChange) {
42 | onChange();
43 | }
44 | dispatch(setTeamPassword(e.target.value));
45 | };
46 |
47 | useEffect(() => {
48 | const listener = (e: KeyboardEvent) => {
49 | if (e.key === 'Enter') {
50 | onSubmitModal();
51 | }
52 | };
53 | if (open) {
54 | document.addEventListener('keydown', listener);
55 | }
56 | return () => document.removeEventListener('keydown', listener);
57 | }, [onSubmitModal, open]);
58 |
59 | return (
60 |
66 |
74 |
75 | );
76 | };
77 |
--------------------------------------------------------------------------------
/src/components/Pagination/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 | import ReactPaginate from 'react-paginate';
3 | import './style.css';
4 | import leftArrow from 'assets/svg/paginateArrowLeft.svg';
5 | import rightArrow from 'assets/svg/paginateArrowRight.svg';
6 |
7 | type PaginationProps = {
8 | pageCount: number;
9 | changePage: (page: number) => void;
10 | page: number;
11 | };
12 |
13 | export const Pagination: FC = ({ pageCount, changePage, page }) => {
14 | return (
15 | }
17 | nextLabel={
}
18 | breakLabel={'...'}
19 | pageCount={pageCount}
20 | initialPage={page}
21 | marginPagesDisplayed={1}
22 | pageRangeDisplayed={2}
23 | containerClassName={'pagination'}
24 | pageClassName={'pageContainer'}
25 | pageLinkClassName={'pageLink'}
26 | activeClassName={'activePageContainer'}
27 | activeLinkClassName={'activePageLink'}
28 | onPageChange={(page) => changePage(page.selected)}
29 | />
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/src/components/PrivateRoute/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 | import { Redirect, Route } from 'react-router-dom';
3 |
4 | type Props = {
5 | isLoggedIn: boolean;
6 | path: string;
7 | component: FC;
8 | exact?: boolean;
9 | newUserCheck?: boolean;
10 | };
11 |
12 | export const PrivateRoute: FC = ({
13 | component: Component,
14 | isLoggedIn,
15 | newUserCheck,
16 | path,
17 | exact,
18 | }) => {
19 | return (
20 | {
24 | if (isLoggedIn && newUserCheck) {
25 | return ;
26 | }
27 | if (isLoggedIn && !newUserCheck) {
28 | return ;
29 | }
30 | if (!isLoggedIn) {
31 | return ;
32 | }
33 | }}
34 | />
35 | );
36 | };
37 |
--------------------------------------------------------------------------------
/src/components/SelectField/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, SelectHTMLAttributes } from 'react';
2 | import { Label, Select, SelectInner } from 'typography';
3 | import { FieldWrapper } from './styled';
4 | //
5 | // for future
6 | // TODO: make separate SelectField component for other screens
7 | // now is not used
8 | //
9 | interface SelectFieldProps extends SelectHTMLAttributes {
10 | labelText: string;
11 | placeholder: string;
12 | multi?: boolean;
13 | register: any;
14 | }
15 |
16 | const SelectField: FC = ({ labelText, placeholder, register, ...rest }) => {
17 | return (
18 |
19 |
20 |
29 |
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/src/components/SelectField/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const FieldWrapper = styled.div`
4 | display: flex;
5 | flex-direction: column;
6 | margin-bottom: 20px;
7 | `;
8 |
9 | export const PlaceholderOption = styled.option`
10 | display: none;
11 | `;
12 |
--------------------------------------------------------------------------------
/src/components/TablePopup/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 | import { StyledPopup, StyledPopupItem } from './styled';
3 |
4 | type TablePopupProps = {
5 | dataLength: number;
6 | popupElements: string[];
7 | };
8 |
9 | export const TablePopup: FC = ({ popupElements, dataLength }) => {
10 | return (
11 |
12 | {popupElements?.map((element: string) => (
13 | {element}
14 | ))}
15 |
16 | );
17 | };
18 |
--------------------------------------------------------------------------------
/src/components/TablePopup/styled.ts:
--------------------------------------------------------------------------------
1 | import { WHITE_COLOR, DARK_TEXT_COLOR, TABLE_POPUP_BORDER_COLOR } from 'appConstants/colors';
2 | import styled from 'styled-components';
3 | import { GeneralAdaptiveFont } from 'typography';
4 |
5 | type TStyledPopup = {
6 | dataLength: number;
7 | };
8 |
9 | export const StyledPopup = styled.div`
10 | position: absolute;
11 | top: ${({ dataLength }) => dataLength < 0.5 && '95%'};
12 | bottom: ${({ dataLength }) => dataLength > 0.5 && '95%'};
13 | right: 0;
14 | z-index: 2;
15 | display: flex;
16 | flex-direction: column;
17 | justify-content: center;
18 | padding: 10px;
19 | font-size: 1rem;
20 | color: ${DARK_TEXT_COLOR};
21 | background-color: ${WHITE_COLOR};
22 | border: 1px solid ${TABLE_POPUP_BORDER_COLOR};
23 | border-radius: 10px;
24 | ${GeneralAdaptiveFont};
25 | @media (max-width: 440px) {
26 | left: -5px;
27 | padding: 5px;
28 | }
29 | `;
30 |
31 | export const StyledPopupItem = styled.p`
32 | margin: 0;
33 | line-height: 17px;
34 | text-align: justify;
35 | `;
36 |
--------------------------------------------------------------------------------
/src/components/TourGuide/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, useCallback, useState } from 'react';
2 | import { useDispatch, useSelector } from 'react-redux';
3 | import { selectIsTourOpen } from 'modules/LoginPage/selectors';
4 | import Tour, { ReactourStep } from 'reactour';
5 | import { tourConfig } from './tourConfig';
6 | import { setIsTourOpen } from 'modules/LoginPage/loginPageReducer';
7 | import { MAIN1_COLOR } from 'appConstants/colors';
8 | import leftArrow from 'assets/svg/paginateArrowLeft.svg';
9 | import rightArrow from 'assets/svg/paginateArrowRight.svg';
10 | import './style.css';
11 | import { useHistory } from 'react-router';
12 | import { useTranslation } from 'react-i18next';
13 |
14 | export const TourGuide: FC = () => {
15 | const [isButtonsVisible, setIsButtonsVisible] = useState(false);
16 | const dispatch = useDispatch();
17 | const isTourOpen = useSelector(selectIsTourOpen);
18 | const history = useHistory();
19 | const { t } = useTranslation();
20 |
21 | const onRequestCloseHandler = () => dispatch(setIsTourOpen(false));
22 |
23 | const tourConfigInfo = useCallback(
24 | () => tourConfig(history, dispatch, t),
25 | [history, dispatch, t]
26 | );
27 |
28 | return (
29 | }
34 | nextButton={
}
35 | accentColor={MAIN1_COLOR}
36 | maskClassName="mask"
37 | disableDotsNavigation
38 | disableKeyboardNavigation
39 | showNavigationNumber={false}
40 | showButtons={isButtonsVisible}
41 | disableInteraction
42 | lastStepNextButton={}
43 | getCurrentStep={(currStep) => setIsButtonsVisible(!!currStep)}
44 | closeWithMask={false}
45 | className="helper"
46 | rounded={20}
47 | />
48 | );
49 | };
50 |
--------------------------------------------------------------------------------
/src/components/TourGuide/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { MAIN1_COLOR, WHITE_COLOR } from 'appConstants/colors';
3 | import { TextSemiBold, GeneralAdaptiveFont, GeneralButtonPadding } from 'typography';
4 |
5 | export const LinkButton = styled.a`
6 | ${TextSemiBold};
7 | margin-right: 0;
8 | border-radius: 20px;
9 | border: none;
10 | text-decoration: none;
11 | outline: none;
12 | cursor: pointer;
13 | background-color: ${MAIN1_COLOR};
14 | color: ${WHITE_COLOR};
15 | ${GeneralAdaptiveFont};
16 | ${GeneralButtonPadding}
17 | `;
18 |
--------------------------------------------------------------------------------
/src/components/TourGuide/tourConfig.tsx:
--------------------------------------------------------------------------------
1 | import { Button, InvertedButton, ButtonsBlock } from 'typography';
2 | import { setIsTourOpen } from 'modules/LoginPage/loginPageReducer';
3 | import { LINK_TO_REPO } from 'appConstants';
4 | import { LinkButton } from './styled';
5 | import { Dispatch } from 'redux';
6 | import { History, LocationState } from 'history';
7 |
8 | export const tourConfig = (
9 | history: History,
10 | dispatch: Dispatch,
11 | t: (text: string) => string
12 | ) => [
13 | {
14 | content: ({ goTo }: { goTo: (step: number) => void }) => (
15 |
16 |
{t('Welcome to RSS Teams')}
17 |
18 | goTo(9)}>{t('Skip')}
19 |
20 |
21 |
22 | ),
23 | action: () => history.push('/'),
24 | },
25 | {
26 | selector: '.secondStep',
27 | content: t('You can create new team'),
28 | action: () => history.push('/'),
29 | },
30 | {
31 | selector: '.thirdStep',
32 | content: t('Or join existing team'),
33 | action: () => history.push('/'),
34 | },
35 | {
36 | selector: '.fourthStep',
37 | content: t('You can always leave the course'),
38 | action: () => history.push('/'),
39 | },
40 | {
41 | content: t('On Teams page you can see other teams'),
42 | action: () => history.push('/'),
43 | },
44 | {
45 | content: t('On Dashboard page'),
46 | action: () => history.push('/students'),
47 | },
48 | {
49 | selector: '.seventhStep',
50 | content: t('With filter usage'),
51 | position: 'bottom',
52 | action: () => history.push('/students'),
53 | },
54 | {
55 | selector: '.eighthStep',
56 | content: t('With dropdown you can switch the course'),
57 | position: 'bottom',
58 | action: () => history.push('/students'),
59 | },
60 | {
61 | content: t('On Edit profile page'),
62 | action: () => history.push('/edit-profile'),
63 | },
64 | {
65 | content: t('If you forget something'),
66 | action: () => history.push('/tutorial'),
67 | },
68 | {
69 | content: () => (
70 |
71 |
72 | {t('Support us')}{' '}
73 |
74 | {t('our repo')}
75 |
76 | !
77 |
78 |
79 | {
81 | dispatch(setIsTourOpen(false));
82 | }}
83 | href={LINK_TO_REPO}
84 | target="_blank"
85 | rel="noreferrer"
86 | >
87 | {t('Got it')}
88 |
89 |
90 |
91 | ),
92 | action: () => history.push('/'),
93 | },
94 | ];
95 |
--------------------------------------------------------------------------------
/src/components/index.ts:
--------------------------------------------------------------------------------
1 | export { App } from './App';
2 | export { Loader } from './Loader';
3 | export { PrivateRoute } from './PrivateRoute';
4 | export { Header } from './Header';
5 | export { InputField } from './InputField';
6 | export { CourseField } from './CourseField';
7 | export { Pagination } from './Pagination';
8 | export { Modal } from './Modal';
9 | export { ModalExpel } from './ModalExpel';
10 | export { ModalJoin } from './ModalJoin';
11 | export { ModalCreateEditTeam } from './ModalCreateEditTeam';
12 | export { ModalCreated } from './ModalCreated';
13 | export { TablePopup } from './TablePopup';
14 | export { FilterForm } from './FilterForm';
15 | export { FilterSelect } from './FilterSelect';
16 | export { Footer } from './Footer';
17 | export { ErrorModal } from './ErrorModal';
18 | export { CommonSelectList } from './CommonSelectList';
19 | export { TourGuide } from './TourGuide';
20 | export { ModalEditCourse } from './ModalEditCourse';
21 |
--------------------------------------------------------------------------------
/src/graphql/mutations/addUserToTeamMutation.ts:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client';
2 |
3 | export const ADD_USER_TO_TEAM_MUTATION = gql`
4 | mutation addUserToTeam($data: AddUserToTeamInput!) {
5 | addUserToTeam(data: $data) {
6 | id
7 | firstName
8 | lastName
9 | github
10 | telegram
11 | discord
12 | score
13 | country
14 | city
15 | isAdmin
16 | courses {
17 | id
18 | name
19 | }
20 | teams {
21 | id
22 | password
23 | number
24 | courseId
25 | socialLink
26 | members {
27 | id
28 | firstName
29 | lastName
30 | github
31 | telegram
32 | discord
33 | score
34 | country
35 | city
36 | }
37 | }
38 | }
39 | }
40 | `;
41 |
--------------------------------------------------------------------------------
/src/graphql/mutations/createCourseMutation.ts:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client';
2 |
3 | export const CREATE_COURSE_MUTATION = gql`
4 | mutation createCourse($course: CreateCourseInput!) {
5 | createCourse(course: $course) {
6 | id
7 | name
8 | teamSize
9 | isActive
10 | }
11 | }
12 | `;
13 |
--------------------------------------------------------------------------------
/src/graphql/mutations/createTeamMutation.ts:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client';
2 |
3 | export const CREATE_TEAM_MUTATION = gql`
4 | mutation createTeam($team: CreateTeamInput!) {
5 | createTeam(team: $team) {
6 | id
7 | password
8 | number
9 | courseId
10 | socialLink
11 | memberIds
12 | members {
13 | id
14 | firstName
15 | lastName
16 | github
17 | telegram
18 | discord
19 | score
20 | country
21 | city
22 | }
23 | }
24 | }
25 | `;
26 |
--------------------------------------------------------------------------------
/src/graphql/mutations/index.ts:
--------------------------------------------------------------------------------
1 | export { UPD_USER_MUTATION } from './updUserMutation';
2 | export { ADD_USER_TO_TEAM_MUTATION } from './addUserToTeamMutation';
3 | export { REMOVE_USER_FROM_TEAM_MUTATION } from './removeUserFromTeamMutation';
4 | export { CREATE_TEAM_MUTATION } from './createTeamMutation';
5 | export { UPDATE_TEAM_MUTATION } from './updateTeamMutation';
6 | export { SORT_STUDENTS_MUTATION } from './sortStudentsMutation';
7 | export { REMOVE_USER_FROM_COURSE_MUTATION } from './removeUserFromCourseMutation';
8 | export { CREATE_COURSE_MUTATION } from './createCourseMutation';
9 | export { UPDATE_COURSE_MUTATION } from './updateCourseMutation';
10 |
--------------------------------------------------------------------------------
/src/graphql/mutations/removeUserFromCourseMutation.ts:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client';
2 |
3 | export const REMOVE_USER_FROM_COURSE_MUTATION = gql`
4 | mutation removeUserFromCourse($data: RemoveUserFromCourseInput!) {
5 | removeUserFromCourse(data: $data) {
6 | id
7 | firstName
8 | lastName
9 | github
10 | telegram
11 | discord
12 | score
13 | country
14 | city
15 | isAdmin
16 | courses {
17 | id
18 | name
19 | }
20 | teams {
21 | id
22 | password
23 | number
24 | courseId
25 | socialLink
26 | members {
27 | id
28 | firstName
29 | lastName
30 | github
31 | telegram
32 | discord
33 | score
34 | country
35 | city
36 | }
37 | }
38 | }
39 | }
40 | `;
41 |
--------------------------------------------------------------------------------
/src/graphql/mutations/removeUserFromTeamMutation.ts:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client';
2 |
3 | export const REMOVE_USER_FROM_TEAM_MUTATION = gql`
4 | mutation removeUserFromTeam($data: RemoveUserFromTeamInput!) {
5 | removeUserFromTeam(data: $data) {
6 | id
7 | firstName
8 | lastName
9 | github
10 | telegram
11 | discord
12 | score
13 | country
14 | city
15 | avatar
16 | isAdmin
17 | courses {
18 | id
19 | name
20 | }
21 | email
22 | courseIds
23 | teamIds
24 | teams {
25 | id
26 | password
27 | number
28 | courseId
29 | socialLink
30 | memberIds
31 | members {
32 | id
33 | firstName
34 | lastName
35 | github
36 | telegram
37 | discord
38 | score
39 | country
40 | city
41 | }
42 | }
43 | }
44 | }
45 | `;
46 |
--------------------------------------------------------------------------------
/src/graphql/mutations/sortStudentsMutation.ts:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client';
2 |
3 | export const SORT_STUDENTS_MUTATION = gql`
4 | mutation sortStudents($courseId: String!) {
5 | sortStudents(courseId: $courseId)
6 | }
7 | `;
8 |
--------------------------------------------------------------------------------
/src/graphql/mutations/updUserMutation.ts:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client';
2 |
3 | export const UPD_USER_MUTATION = gql`
4 | mutation updateUser($user: UpdateUserInput!) {
5 | updateUser(user: $user) {
6 | id
7 | firstName
8 | lastName
9 | github
10 | telegram
11 | discord
12 | score
13 | country
14 | city
15 | isAdmin
16 | courses {
17 | id
18 | name
19 | }
20 | teams {
21 | id
22 | password
23 | number
24 | courseId
25 | socialLink
26 | members {
27 | id
28 | firstName
29 | lastName
30 | github
31 | telegram
32 | discord
33 | score
34 | country
35 | city
36 | }
37 | }
38 | }
39 | }
40 | `;
41 |
--------------------------------------------------------------------------------
/src/graphql/mutations/updateCourseMutation.ts:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client';
2 |
3 | export const UPDATE_COURSE_MUTATION = gql`
4 | mutation updateCourse($course: UpdateCourseInput!) {
5 | updateCourse(course: $course) {
6 | id
7 | name
8 | teamSize
9 | isActive
10 | }
11 | }
12 | `;
13 |
--------------------------------------------------------------------------------
/src/graphql/mutations/updateTeamMutation.ts:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client';
2 |
3 | export const UPDATE_TEAM_MUTATION = gql`
4 | mutation updateTeam($team: UpdateTeamInput!) {
5 | updateTeam(team: $team) {
6 | id
7 | password
8 | number
9 | courseId
10 | socialLink
11 | memberIds
12 | members {
13 | id
14 | firstName
15 | lastName
16 | github
17 | telegram
18 | discord
19 | score
20 | country
21 | city
22 | }
23 | }
24 | }
25 | `;
26 |
--------------------------------------------------------------------------------
/src/graphql/queries/coursesQuery.ts:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client';
2 |
3 | export const COURSES_QUERY = gql`
4 | query getCourses {
5 | courses {
6 | id
7 | name
8 | teamSize
9 | isActive
10 | }
11 | }
12 | `;
13 |
--------------------------------------------------------------------------------
/src/graphql/queries/index.ts:
--------------------------------------------------------------------------------
1 | export { TEAMS_QUERY } from './teamsQuery';
2 | export { USERS_QUERY } from './usersQuery';
3 | // export { USER_QUERY } from './userQuery';
4 | export { WHOAMI_QUERY } from './whoAmIQuery';
5 | export { COURSES_QUERY } from './coursesQuery';
6 |
--------------------------------------------------------------------------------
/src/graphql/queries/teamsQuery.ts:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client';
2 |
3 | export const TEAMS_QUERY = gql`
4 | query getTeams($courseId: String!, $pagination: PaginationInput!) {
5 | teams(courseId: $courseId, pagination: $pagination) {
6 | count
7 | results {
8 | id
9 | number
10 | courseId
11 | socialLink
12 | members {
13 | id
14 | firstName
15 | lastName
16 | github
17 | telegram
18 | discord
19 | score
20 | country
21 | city
22 | }
23 | }
24 | }
25 | }
26 | `;
27 |
--------------------------------------------------------------------------------
/src/graphql/queries/usersQuery.ts:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client';
2 |
3 | export const USERS_QUERY = gql`
4 | query getUsers($courseId: String!, $pagination: PaginationInput!, $filter: UserFilterInput) {
5 | users(courseId: $courseId, pagination: $pagination, filter: $filter) {
6 | count
7 | results {
8 | id
9 | firstName
10 | lastName
11 | github
12 | telegram
13 | discord
14 | score
15 | country
16 | city
17 | isAdmin
18 | courses {
19 | id
20 | name
21 | }
22 | teams {
23 | id
24 | number
25 | courseId
26 | }
27 | }
28 | }
29 | }
30 | `;
31 |
--------------------------------------------------------------------------------
/src/graphql/queries/whoAmIQuery.ts:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client';
2 |
3 | export const WHOAMI_QUERY = gql`
4 | query getWhoAMi {
5 | whoAmI {
6 | id
7 | firstName
8 | lastName
9 | github
10 | telegram
11 | discord
12 | score
13 | country
14 | city
15 | isAdmin
16 | courses {
17 | id
18 | name
19 | }
20 | teams {
21 | id
22 | password
23 | number
24 | courseId
25 | socialLink
26 | members {
27 | id
28 | firstName
29 | lastName
30 | github
31 | telegram
32 | discord
33 | score
34 | country
35 | city
36 | }
37 | }
38 | }
39 | }
40 | `;
41 |
--------------------------------------------------------------------------------
/src/hooks/graphql/index.ts:
--------------------------------------------------------------------------------
1 | export { useTeamsQuery } from './queries/useTeamsQuery';
2 | export { useUsersQuery } from './queries/useUsersQuery';
3 | export { useWhoAmIQuery } from './queries/useWhoAmIQuery';
4 | export { useCoursesQuery } from './queries/useCoursesQuery';
5 | export { useUpdUserMutation } from './mutations/useUpdUserMutation';
6 | export { useRemoveUserFromTeamMutation } from './mutations/useRemoveUserFromTeamMutation';
7 | export { useExpelUserFromTeamMutation } from './mutations/useExpelUserFromTeamMutation';
8 | export { useCreateTeamMutation } from './mutations/useCreateTeamMutation';
9 | export { useAddUserToTeamMutation } from './mutations/useAddUserToTeamMutation';
10 | export { useUpdateTeamMutation } from './mutations/useUpdateTeamMutation';
11 | export { useSortStudentsMutation } from './mutations/useSortStudentsMutation';
12 | export { useRemoveUserFromCourseMutation } from './mutations/useRemoveUserFromCourseMutation';
13 | export { useCreateCourseMutation } from './mutations/useCreateCourseMutation';
14 | export { useUpdateCourseMutation } from './mutations/useUpdateCourseMutation';
15 |
--------------------------------------------------------------------------------
/src/hooks/graphql/mutations/useAddUserToTeamMutation.ts:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@apollo/client';
2 | import { ADD_USER_TO_TEAM_MUTATION } from 'graphql/mutations';
3 | import { WHOAMI_QUERY } from 'graphql/queries';
4 | import { AddUserToTeamInput } from 'types';
5 |
6 | type Props = {
7 | data: AddUserToTeamInput;
8 | };
9 |
10 | export const useAddUserToTeamMutation = ({ data }: Props) => {
11 | const [addUserToTeam, { loading, error }] = useMutation(ADD_USER_TO_TEAM_MUTATION, {
12 | variables: {
13 | data,
14 | },
15 |
16 | update(cache, { data: { addUserToTeam } }) {
17 | cache.writeQuery({
18 | query: WHOAMI_QUERY,
19 | data: {
20 | addUserToTeam,
21 | },
22 | });
23 | },
24 | });
25 | return {
26 | addUserToTeam,
27 | loadingM: loading,
28 | errorM: error,
29 | };
30 | };
31 |
--------------------------------------------------------------------------------
/src/hooks/graphql/mutations/useCreateCourseMutation.ts:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@apollo/client';
2 | import { Course, CreateCourseInput } from 'types';
3 | import { COURSES_QUERY } from 'graphql/queries';
4 | import { CREATE_COURSE_MUTATION } from 'graphql/mutations';
5 |
6 | type Props = {
7 | course: CreateCourseInput;
8 | };
9 |
10 | export const useCreateCourseMutation = ({ course }: Props) => {
11 | const [createCourse, { loading, error }] = useMutation(CREATE_COURSE_MUTATION, {
12 | variables: {
13 | course,
14 | },
15 |
16 | update(cache, { data: { createCourse } }) {
17 | const data: { courses: Course[] } | null = cache.readQuery({
18 | query: COURSES_QUERY,
19 | });
20 |
21 | const updatedResults = data?.courses?.length
22 | ? [createCourse, ...data?.courses]
23 | : [createCourse];
24 |
25 | cache.writeQuery({
26 | query: COURSES_QUERY,
27 | data: {
28 | courses: updatedResults,
29 | },
30 | });
31 | },
32 | });
33 | return {
34 | createCourse,
35 | loadingM: loading,
36 | errorM: error,
37 | };
38 | };
39 |
--------------------------------------------------------------------------------
/src/hooks/graphql/mutations/useCreateTeamMutation.ts:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@apollo/client';
2 | import { CreateTeamInput, TeamList, User } from 'types';
3 | import { TEAMS_QUERY, WHOAMI_QUERY } from 'graphql/queries';
4 | import { CREATE_TEAM_MUTATION } from 'graphql/mutations';
5 | import { TEAMS_PER_PAGE } from 'appConstants';
6 |
7 | type Props = {
8 | team: CreateTeamInput;
9 | };
10 |
11 | export const useCreateTeamMutation = ({ team }: Props) => {
12 | const { courseId, ownerId, socialLink, page } = team;
13 | const dataForMutation = { courseId, ownerId, socialLink };
14 | const [createTeam, { loading, error }] = useMutation(CREATE_TEAM_MUTATION, {
15 | variables: {
16 | team: dataForMutation,
17 | },
18 |
19 | update(cache, { data: { createTeam } }) {
20 | const data: { teams: TeamList } | null = cache.readQuery({
21 | query: TEAMS_QUERY,
22 | variables: {
23 | courseId: courseId,
24 | pagination: { skip: page * TEAMS_PER_PAGE, take: TEAMS_PER_PAGE },
25 | },
26 | });
27 | const userData: { whoAmI: User } | null = cache.readQuery({
28 | query: WHOAMI_QUERY,
29 | });
30 |
31 | const updatedResults = data?.teams?.results.length
32 | ? [...data?.teams?.results, createTeam]
33 | : [createTeam];
34 |
35 | const updatedUser = {
36 | ...userData?.whoAmI,
37 | teams: userData?.whoAmI?.teams.length
38 | ? [...userData?.whoAmI?.teams, createTeam]
39 | : [createTeam],
40 | };
41 |
42 | cache.writeQuery({
43 | query: WHOAMI_QUERY,
44 | data: {
45 | whoAmI: updatedUser,
46 | },
47 | });
48 |
49 | cache.writeQuery({
50 | query: TEAMS_QUERY,
51 | data: {
52 | teams: {
53 | count: updatedResults.length,
54 | results: updatedResults,
55 | },
56 | },
57 | variables: {
58 | courseId: courseId,
59 | pagination: { skip: page * TEAMS_PER_PAGE, take: TEAMS_PER_PAGE },
60 | },
61 | });
62 | },
63 | });
64 | return {
65 | createTeam,
66 | loadingM: loading,
67 | errorM: error,
68 | };
69 | };
70 |
--------------------------------------------------------------------------------
/src/hooks/graphql/mutations/useExpelUserFromTeamMutation.ts:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@apollo/client';
2 | import { RemoveUserFromTeamInput, Team, TeamList, User } from 'types';
3 | import { TEAMS_QUERY, WHOAMI_QUERY } from 'graphql/queries';
4 | import { REMOVE_USER_FROM_TEAM_MUTATION } from 'graphql/mutations';
5 | import { TEAMS_PER_PAGE } from 'appConstants';
6 |
7 | type Props = {
8 | data: RemoveUserFromTeamInput;
9 | };
10 |
11 | export const useExpelUserFromTeamMutation = ({ data }: Props) => {
12 | const { teamId, page, userId, courseId } = data;
13 | const dataForMutation = { userId, teamId };
14 | const [expelUserFromTeam, { loading, error }] = useMutation(REMOVE_USER_FROM_TEAM_MUTATION, {
15 | variables: {
16 | data: dataForMutation,
17 | },
18 |
19 | update(cache, { data: {} }) {
20 | const data: { teams: TeamList } | null = cache.readQuery({
21 | query: TEAMS_QUERY,
22 | variables: {
23 | courseId: courseId,
24 | pagination: { skip: page * TEAMS_PER_PAGE, take: TEAMS_PER_PAGE },
25 | },
26 | });
27 |
28 | const userData: { whoAmI: User } | null = cache.readQuery({
29 | query: WHOAMI_QUERY,
30 | });
31 |
32 | const updatedRemovedResults = data?.teams.results.map((team: Team) => {
33 | if (team.id === teamId) {
34 | return {
35 | ...team,
36 | members: team.members.filter((member: User) => member.id !== userId),
37 | };
38 | }
39 | return team;
40 | });
41 |
42 | const updatedTeams = (userData?.whoAmI.teams as Team[]).map((team: Team) => {
43 | if (team.id === teamId) {
44 | return {
45 | ...team,
46 | members: team.members.filter((member: User) => member.id !== userId),
47 | };
48 | }
49 | return team;
50 | });
51 |
52 | cache.writeQuery({
53 | query: WHOAMI_QUERY,
54 | data: {
55 | ...userData?.whoAmI,
56 | teams: updatedTeams,
57 | },
58 | });
59 |
60 | cache.writeQuery({
61 | query: TEAMS_QUERY,
62 | data: {
63 | teams: {
64 | count: data?.teams?.count,
65 | results: updatedRemovedResults,
66 | },
67 | },
68 | variables: {
69 | courseId: courseId,
70 | pagination: { skip: page * TEAMS_PER_PAGE, take: TEAMS_PER_PAGE },
71 | },
72 | });
73 | },
74 | });
75 | return {
76 | expelUserFromTeam,
77 | loadingM: loading,
78 | errorM: error,
79 | };
80 | };
81 |
--------------------------------------------------------------------------------
/src/hooks/graphql/mutations/useRemoveUserFromTeamMutation.ts:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@apollo/client';
2 | import { RemoveUserFromTeamInput, Team, TeamList, User } from 'types';
3 | import { TEAMS_QUERY, WHOAMI_QUERY } from 'graphql/queries';
4 | import { REMOVE_USER_FROM_TEAM_MUTATION } from 'graphql/mutations';
5 | import { TEAMS_PER_PAGE } from 'appConstants';
6 |
7 | type Props = {
8 | data: RemoveUserFromTeamInput;
9 | };
10 |
11 | export const useRemoveUserFromTeamMutation = ({ data }: Props) => {
12 | const { teamId, page, userId, courseId } = data;
13 | const dataForMutation = { userId, teamId };
14 | const [removeUserFromTeam, { loading, error }] = useMutation(REMOVE_USER_FROM_TEAM_MUTATION, {
15 | variables: {
16 | data: dataForMutation,
17 | },
18 |
19 | update(cache, { data: { removeUserFromTeam } }) {
20 | const data: { teams: TeamList } | null = cache.readQuery({
21 | query: TEAMS_QUERY,
22 | variables: {
23 | courseId: courseId,
24 | pagination: { skip: page * TEAMS_PER_PAGE, take: TEAMS_PER_PAGE },
25 | },
26 | });
27 |
28 | const updatedRemovedResults = data?.teams.results
29 | .map((team: Team) => {
30 | if (team.id === teamId) {
31 | return {
32 | ...team,
33 | members: team.members.filter((member: User) => member.id !== userId),
34 | };
35 | }
36 | return team;
37 | })
38 | .filter((team: Team | undefined) => !!team?.members.length);
39 |
40 | cache.writeQuery({
41 | query: WHOAMI_QUERY,
42 | data: {
43 | removeUserFromTeam,
44 | },
45 | });
46 |
47 | cache.writeQuery({
48 | query: TEAMS_QUERY,
49 | data: {
50 | teams: {
51 | count: updatedRemovedResults?.length,
52 | results: updatedRemovedResults,
53 | },
54 | },
55 | variables: {
56 | courseId: courseId,
57 | pagination: { skip: page * TEAMS_PER_PAGE, take: TEAMS_PER_PAGE },
58 | },
59 | });
60 | },
61 | });
62 | return {
63 | removeUserFromTeam,
64 | loadingM: loading,
65 | errorM: error,
66 | };
67 | };
68 |
--------------------------------------------------------------------------------
/src/hooks/graphql/mutations/useSortStudentsMutation.ts:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@apollo/client';
2 | import { SORT_STUDENTS_MUTATION } from 'graphql/mutations';
3 |
4 | type Props = {
5 | courseId: string;
6 | };
7 |
8 | export const useSortStudentsMutation = ({ courseId }: Props) => {
9 | const [sortStudents, { loading, error }] = useMutation(SORT_STUDENTS_MUTATION, {
10 | variables: {
11 | courseId,
12 | },
13 | });
14 | return {
15 | sortStudents,
16 | loadingM: loading,
17 | errorM: error,
18 | };
19 | };
20 |
--------------------------------------------------------------------------------
/src/hooks/graphql/mutations/useUpdUserMutation.ts:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@apollo/client';
2 | import { UPD_USER_MUTATION } from 'graphql/mutations';
3 | import { WHOAMI_QUERY } from 'graphql/queries';
4 | import { UpdateUserInput } from 'types';
5 |
6 | type Props = {
7 | user: UpdateUserInput;
8 | };
9 |
10 | export const useUpdUserMutation = ({ user }: Props) => {
11 | const formattedUser = { ...user, score: Number(user.score) };
12 | const [updateUser, { loading, error }] = useMutation(UPD_USER_MUTATION, {
13 | variables: {
14 | user: formattedUser,
15 | },
16 | update(cache, { data: { updateUser } }) {
17 | cache.writeQuery({
18 | query: WHOAMI_QUERY,
19 | data: {
20 | updateUser,
21 | },
22 | });
23 | },
24 | });
25 | return {
26 | updateUser,
27 | loadingM: loading,
28 | errorM: error,
29 | };
30 | };
31 |
--------------------------------------------------------------------------------
/src/hooks/graphql/mutations/useUpdateCourseMutation.ts:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@apollo/client';
2 | import { UpdateCourseInput, Course } from 'types';
3 | import { COURSES_QUERY } from 'graphql/queries';
4 | import { UPDATE_COURSE_MUTATION } from 'graphql/mutations';
5 |
6 | type Props = {
7 | course: UpdateCourseInput;
8 | };
9 |
10 | export const useUpdateCourseMutation = ({ course }: Props) => {
11 | const { id } = course;
12 | const [updateCourse, { loading, error }] = useMutation(UPDATE_COURSE_MUTATION, {
13 | variables: {
14 | course,
15 | },
16 |
17 | update(cache, { data: { updateCourse } }) {
18 | const data: { courses: Course[] } | null = cache.readQuery({
19 | query: COURSES_QUERY,
20 | });
21 |
22 | const updatedResults = data?.courses?.map((course: Course) => {
23 | if (course.id === id) {
24 | return updateCourse;
25 | }
26 | return course;
27 | });
28 |
29 | cache.writeQuery({
30 | query: COURSES_QUERY,
31 | data: {
32 | courses: updatedResults,
33 | },
34 | });
35 | },
36 | });
37 | return {
38 | updateCourse,
39 | loadingM: loading,
40 | errorM: error,
41 | };
42 | };
43 |
--------------------------------------------------------------------------------
/src/hooks/graphql/mutations/useUpdateTeamMutation.ts:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@apollo/client';
2 | import { UpdateTeamInput, User, Team } from 'types';
3 | import { WHOAMI_QUERY } from 'graphql/queries';
4 | import { UPDATE_TEAM_MUTATION } from 'graphql/mutations';
5 |
6 | type Props = {
7 | team: UpdateTeamInput;
8 | };
9 |
10 | export const useUpdateTeamMutation = ({ team }: Props) => {
11 | const { id } = team;
12 | const [updateTeam, { loading, error }] = useMutation(UPDATE_TEAM_MUTATION, {
13 | variables: {
14 | team,
15 | },
16 |
17 | update(cache, { data: { updateTeam } }) {
18 | const userData: { whoAmI: User } | null = cache.readQuery({
19 | query: WHOAMI_QUERY,
20 | });
21 |
22 | const updatedUserTeam = (userData?.whoAmI.teams as Team[]).map((team: Team) => {
23 | if (team.id === id) {
24 | return updateTeam;
25 | }
26 | return team;
27 | });
28 |
29 | cache.writeQuery({
30 | query: WHOAMI_QUERY,
31 | data: {
32 | whoAmI: {
33 | ...userData?.whoAmI,
34 | teams: updatedUserTeam,
35 | },
36 | },
37 | });
38 | },
39 | });
40 | return {
41 | updateTeam,
42 | loadingM: loading,
43 | errorM: error,
44 | };
45 | };
46 |
--------------------------------------------------------------------------------
/src/hooks/graphql/queries/useCoursesQuery.ts:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@apollo/client';
2 |
3 | import { COURSES_QUERY } from 'graphql/queries';
4 |
5 | export const useCoursesQuery = () => {
6 | const { data, loading, error } = useQuery(COURSES_QUERY);
7 | const isLoaded = !loading && data?.courses;
8 |
9 | return {
10 | loading: !isLoaded,
11 | error,
12 | courses: data?.courses,
13 | };
14 | };
15 |
--------------------------------------------------------------------------------
/src/hooks/graphql/queries/useTeamsQuery.ts:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@apollo/client';
2 | import { TEAMS_PER_PAGE } from 'appConstants';
3 |
4 | import { TEAMS_QUERY } from 'graphql/queries';
5 | type Props = {
6 | reactCourseId: string;
7 | skip?: boolean;
8 | page?: number;
9 | };
10 |
11 | export const useTeamsQuery = ({ reactCourseId, skip = false, page = 0 }: Props) => {
12 | const { data, loading, error } = useQuery(TEAMS_QUERY, {
13 | skip,
14 | variables: {
15 | courseId: reactCourseId,
16 | pagination: {
17 | skip: page * TEAMS_PER_PAGE,
18 | take: TEAMS_PER_PAGE,
19 | },
20 | },
21 | });
22 | const isLoaded = !loading && !!data;
23 | return {
24 | loadingT: !isLoaded,
25 | errorT: error,
26 | teams: data?.teams,
27 | };
28 | };
29 |
--------------------------------------------------------------------------------
/src/hooks/graphql/queries/useUsersQuery.ts:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@apollo/client';
2 | import { USERS_PER_PAGE } from 'appConstants';
3 |
4 | import { USERS_QUERY } from 'graphql/queries';
5 | import { UserFilterInput } from 'types';
6 |
7 | type Props = {
8 | reactCourseId: string;
9 | skip?: boolean;
10 | page?: number;
11 | filter?: UserFilterInput;
12 | };
13 |
14 | export const useUsersQuery = ({ reactCourseId, skip = false, page = 0, filter }: Props) => {
15 | const { data, loading, error } = useQuery(USERS_QUERY, {
16 | skip,
17 | variables: {
18 | filter,
19 | courseId: reactCourseId,
20 | pagination: {
21 | skip: page * USERS_PER_PAGE,
22 | take: USERS_PER_PAGE,
23 | },
24 | },
25 | });
26 | const isLoaded = !loading && !!data;
27 |
28 | return {
29 | loadingU: !isLoaded,
30 | errorU: error,
31 | users: data?.users,
32 | };
33 | };
34 |
--------------------------------------------------------------------------------
/src/hooks/graphql/queries/useWhoAmIQuery.ts:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@apollo/client';
2 | import { WHOAMI_QUERY } from 'graphql/queries';
3 |
4 | type Props = {
5 | skip: boolean;
6 | };
7 |
8 | export const useWhoAmIQuery = ({ skip = false }: Props) => {
9 | const {
10 | data,
11 | loading: loadingW,
12 | error,
13 | } = useQuery(WHOAMI_QUERY, {
14 | skip,
15 | });
16 |
17 | return {
18 | loadingW,
19 | errorW: error,
20 | whoAmI: data?.whoAmI,
21 | };
22 | };
23 |
--------------------------------------------------------------------------------
/src/modules/AdminPage/components/ContentWrapper/components/AddCourseBlock/components/InputsBlock/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 | import { useTranslation } from 'react-i18next';
3 | import { ModalInput, Label } from 'typography';
4 | import { ValidationAlert } from 'components/InputField/styled';
5 | import { InputWrapper, FieldWrapper } from './styled';
6 |
7 | interface InputBlockProps {
8 | inputLabel: string;
9 | value: string;
10 | placeholder: string;
11 | onChangeHandler: (e: React.ChangeEvent) => void;
12 | isFieldValid: boolean;
13 | errorMessage: string;
14 | isModal?: boolean;
15 | }
16 |
17 | export const InputBlock: FC = ({
18 | inputLabel,
19 | value,
20 | placeholder,
21 | onChangeHandler,
22 | isFieldValid,
23 | errorMessage,
24 | isModal,
25 | }) => {
26 | const { t } = useTranslation();
27 |
28 | return (
29 |
30 |
31 |
32 |
41 | {!isFieldValid && {t(errorMessage)}}
42 |
43 |
44 | );
45 | };
46 |
--------------------------------------------------------------------------------
/src/modules/AdminPage/components/ContentWrapper/components/AddCourseBlock/components/InputsBlock/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const InputWrapper = styled.div<{ isModal?: boolean }>`
4 | display: flex;
5 | ${({ isModal }) => isModal && 'flex-direction: column;'}
6 | justify-content: space-between;
7 | align-items: center;
8 | width: 100%;
9 | height: fit-content;
10 | ${({ isModal }) => isModal && 'margin-bottom: 40px;'}
11 | gap: ${({ isModal }) => (isModal ? '5px' : '20px')};
12 |
13 | @media (max-width: 580px) {
14 | ${({ isModal }) => isModal && 'margin-bottom: 30px;'}
15 | label {
16 | ${({ isModal }) => !isModal && 'display: none;'}
17 | }
18 | }
19 | `;
20 |
21 | export const FieldWrapper = styled.div`
22 | position: relative;
23 | display: flex;
24 | flex-direction: column;
25 |
26 | & > div:nth-child(2) {
27 | position: absolute;
28 | top: 100%;
29 | }
30 | `;
31 |
--------------------------------------------------------------------------------
/src/modules/AdminPage/components/ContentWrapper/components/AddCourseBlock/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const AddCourseBlockWrapper = styled.div`
4 | display: flex;
5 | align-items: center;
6 | justify-content: center;
7 | width: 100%;
8 | height: 250px;
9 | gap: 4%;
10 |
11 | @media (max-width: 850px) {
12 | flex-direction: column;
13 | }
14 | `;
15 |
16 | export const InputsBlockWrapper = styled.div`
17 | display: flex;
18 | flex-direction: column;
19 | align-items: center;
20 | justify-content: center;
21 | width: fit-content;
22 | height: fit-content;
23 | gap: 20px;
24 |
25 | @media (max-width: 850px) {
26 | margin-bottom: 30px;
27 | }
28 | `;
29 |
--------------------------------------------------------------------------------
/src/modules/AdminPage/components/ContentWrapper/components/CoursesList/components/Course/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 | import { useTranslation } from 'react-i18next';
3 | import { useDispatch } from 'react-redux';
4 | import { CourseButton } from 'typography';
5 | import { CourseWrapper } from './styled';
6 | import { activeModalEditCourse, activeModalSortStudents } from 'modules/TeamsList/teamsListReducer';
7 | import { Course } from 'types';
8 |
9 | interface ICourseItem {
10 | index: number;
11 | id: string;
12 | name: string;
13 | teamSize: number | null;
14 | isActive: boolean;
15 | setCourseInfoToSort: (course: Partial) => void;
16 | setCourseEditMeta: (course: Course | null) => void;
17 | }
18 |
19 | export const CourseItem: FC = ({
20 | index,
21 | id,
22 | name,
23 | teamSize,
24 | isActive,
25 | setCourseInfoToSort,
26 | setCourseEditMeta,
27 | }) => {
28 | const dispatch = useDispatch();
29 | const { t } = useTranslation();
30 | const courseOrder = index + 1;
31 | const courseState = isActive ? 'Active(status)' : 'Terminate(status)';
32 |
33 | const onClickSortStudents = () => {
34 | setCourseInfoToSort({ id, name });
35 | dispatch(activeModalSortStudents(true));
36 | };
37 |
38 | const onClickEditCourse = () => {
39 | setCourseEditMeta({ id, name, isActive, teamSize });
40 | dispatch(activeModalEditCourse(true));
41 | };
42 |
43 | return (
44 |
45 | {courseOrder}
46 | {name}
47 | {teamSize ?? t('Unset')}
48 | {t(courseState)}
49 | {t('Edit course')}
50 | {t('Sort students')}
51 |
52 | );
53 | };
54 |
--------------------------------------------------------------------------------
/src/modules/AdminPage/components/ContentWrapper/components/CoursesList/components/Course/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { BG_COLOR } from 'appConstants/colors';
3 | import { GeneralAdaptiveFont } from 'typography';
4 |
5 | export const CourseWrapper = styled.div`
6 | display: flex;
7 | justify-content: space-between;
8 | align-items: center;
9 | width: 90%;
10 | background-color: ${BG_COLOR};
11 | padding: 20px;
12 | border-radius: 20px;
13 |
14 | & > div:nth-child(1) {
15 | font-weight: 600;
16 | }
17 |
18 | & > div {
19 | ${GeneralAdaptiveFont};
20 | }
21 |
22 | @media (max-width: 1100px) {
23 | flex-direction: column;
24 | width: 40%;
25 | gap: 10px;
26 |
27 | & > div:nth-child(1) {
28 | text-align: left;
29 | }
30 |
31 | button,
32 | div {
33 | width: 90%;
34 | text-align: center;
35 | }
36 | }
37 |
38 | @media (max-width: 768px) {
39 | width: 80%;
40 | }
41 |
42 | @media (max-width: 450px) {
43 | gap: 5px;
44 | }
45 | `;
46 |
--------------------------------------------------------------------------------
/src/modules/AdminPage/components/ContentWrapper/components/CoursesList/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const CoursesListWrapper = styled.div`
4 | display: flex;
5 | flex-direction: column;
6 | justify-content: center;
7 | align-items: center;
8 | width: 100%;
9 | gap: 20px;
10 |
11 | @media (max-width: 1100px) and (min-width: 768px) {
12 | flex-wrap: wrap;
13 | flex-direction: row;
14 | align-items: stretch;
15 | }
16 | `;
17 |
--------------------------------------------------------------------------------
/src/modules/AdminPage/components/ContentWrapper/components/ShowCourseSelect/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, useState } from 'react';
2 | import { SHOW_COURSES_OPTIONS } from 'appConstants';
3 | import { CommonSelectList } from 'components';
4 |
5 | interface ShowCourseSelectProps {
6 | currentOption: string;
7 | setCurrentOption: (newOption: string) => void;
8 | }
9 |
10 | export const ShowCourseSelect: FC = ({
11 | currentOption,
12 | setCurrentOption,
13 | }) => {
14 | const [isSelectOpen, setIsSelectOpen] = useState(false);
15 |
16 | const onOptionChange = (item: { id: string; name: string }) => {
17 | setCurrentOption(item.name);
18 | };
19 |
20 | const options: { id: string; name: string }[] = SHOW_COURSES_OPTIONS.filter(
21 | (option) => option.name !== currentOption
22 | );
23 |
24 | return (
25 |
35 | );
36 | };
37 |
--------------------------------------------------------------------------------
/src/modules/AdminPage/components/ContentWrapper/styled.ts:
--------------------------------------------------------------------------------
1 | import { DARK_TEXT_COLOR, WHITE_COLOR } from 'appConstants/colors';
2 | import styled from 'styled-components';
3 | import { H2AdaptiveFont } from 'typography';
4 |
5 | export const AdminPageContentWrapper = styled.div`
6 | position: relative;
7 | z-index: 0;
8 | display: flex;
9 | flex-direction: column;
10 | width: 100%;
11 | max-width: 1320px;
12 | min-height: 75vh;
13 | padding: 20px 0;
14 | background-color: ${WHITE_COLOR};
15 | border-radius: 20px;
16 | `;
17 |
18 | export const ListTitle = styled.h4`
19 | ${H2AdaptiveFont}
20 | font-size: 25px;
21 | color: ${DARK_TEXT_COLOR};
22 | text-align: center;
23 | `;
24 |
25 | export const CourseListSettings = styled.div`
26 | position: relative;
27 | display: flex;
28 | justify-content: flex-end;
29 | padding: 10px 5% 30px;
30 |
31 | @media (max-width: 1100px) {
32 | justify-content: flex-start;
33 | padding: 10px 9% 80px;
34 | }
35 | @media (max-width: 768px) {
36 | padding: 10px 10% 80px;
37 | }
38 | `;
39 |
--------------------------------------------------------------------------------
/src/modules/AdminPage/components/index.ts:
--------------------------------------------------------------------------------
1 | export { AdminPageWrapper } from './ContentWrapper';
2 |
--------------------------------------------------------------------------------
/src/modules/AdminPage/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 | import { TeamsTitleWrapper } from 'modules/TeamsList/styled';
3 | import { useTranslation } from 'react-i18next';
4 | import { ContentPageWrapper } from 'typography';
5 | import { StudentTableWrapper, TableTitle } from 'modules/StudentsTable/styled';
6 | import { AdminPageWrapper } from './components';
7 |
8 | export const AdminPage: FC = () => {
9 | const { t } = useTranslation();
10 |
11 | return (
12 |
13 |
14 |
15 | {t('Admin page')}
16 |
17 |
18 |
19 |
20 | );
21 | };
22 |
--------------------------------------------------------------------------------
/src/modules/EditProfile/components/UserCourseListItem/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 | import { useSelector } from 'react-redux';
3 | import { MinusButton, UserCourseListItemStyled } from './styled';
4 | import { Course, Team } from 'types';
5 | import { ReactComponent as CrossSvgIcon } from 'assets/svg/cross.svg';
6 | import { selectUserData } from 'modules/StudentsTable/selectors';
7 | import { useRemoveUserFromCourseMutation } from 'hooks/graphql';
8 |
9 | type TUserCourseListItem = {
10 | isUserRegisteredCourse: boolean;
11 | onSub: (c: IOldCourses) => void;
12 | course: Course;
13 | };
14 |
15 | export interface IOldCourses extends Course {
16 | isNew: boolean;
17 | }
18 |
19 | export const UserCourseListItem: FC = ({
20 | children,
21 | isUserRegisteredCourse,
22 | onSub,
23 | course,
24 | }) => {
25 | const userData = useSelector(selectUserData);
26 |
27 | const { removeUserFromCourse } = useRemoveUserFromCourseMutation({
28 | data: {
29 | courseId: course.id,
30 | userId: userData.id,
31 | teamId: userData.teams.find((team: Team) => team.courseId === course.id)?.id ?? null,
32 | page: 0,
33 | },
34 | });
35 | const onClickHandler = isUserRegisteredCourse
36 | ? () => {
37 | onSub({ ...course, isNew: false });
38 | removeUserFromCourse();
39 | }
40 | : () => {
41 | onSub({ ...course, isNew: true });
42 | };
43 |
44 | return (
45 |
46 | {children}
47 |
48 |
49 |
50 |
51 | );
52 | };
53 |
--------------------------------------------------------------------------------
/src/modules/EditProfile/components/UserCourseListItem/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { BG_COLOR, DARK_TEXT_COLOR } from 'appConstants/colors';
3 | import { PlusButton } from 'components/CourseField/styled';
4 | import { GeneralAdaptiveFont } from 'typography';
5 |
6 | export const UserCourseListItemStyled = styled.div`
7 | display: flex;
8 |
9 | div {
10 | flex-grow: 1;
11 | padding: 8px 15px;
12 | margin-bottom: 20px;
13 | border-radius: 10px;
14 | background-color: ${BG_COLOR};
15 | color: ${DARK_TEXT_COLOR};
16 | ${GeneralAdaptiveFont}
17 | }
18 | `;
19 |
20 | export const MinusButton = styled(PlusButton)`
21 | background-image: none;
22 | display: flex;
23 | align-items: center;
24 | justify-content: center;
25 | `;
26 |
--------------------------------------------------------------------------------
/src/modules/EditProfile/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { BG_COLOR, DARK_TEXT_COLOR, LIGHT_TEXT_COLOR, WHITE_COLOR } from 'appConstants/colors';
3 | import {
4 | PageTitle,
5 | H1AdaptiveFont,
6 | GeneralAdaptiveFont,
7 | ScrollBar,
8 | MainComponentHeight,
9 | } from 'typography';
10 |
11 | export const EditProfileWrapper = styled.form`
12 | background-color: ${WHITE_COLOR};
13 | width: 680px;
14 | padding: 30px;
15 | border-radius: 20px;
16 | margin: 75px 0 15px;
17 | @media screen and (max-width: 768px) {
18 | width: 320px;
19 | padding: 15px 10px;
20 | flex-direction: column;
21 | margin: 50px 0 30px;
22 | }
23 |
24 | @media (max-width: 440px) {
25 | width: 280px;
26 | }
27 | `;
28 |
29 | export const FormWrapper = styled.div`
30 | position: relative;
31 | display: flex;
32 | justify-content: center;
33 | align-items: center;
34 | width: 100%;
35 | overflow-y: scroll;
36 | ${ScrollBar};
37 | ${MainComponentHeight};
38 | `;
39 |
40 | export const FormTitle = styled(PageTitle)`
41 | margin-top: 0;
42 | margin-bottom: 32px;
43 | ${H1AdaptiveFont};
44 | `;
45 |
46 | export const InputsWrapper = styled.div`
47 | display: flex;
48 | flex-direction: row;
49 | justify-content: space-between;
50 | flex-wrap: wrap;
51 | margin-bottom: 10px;
52 | @media screen and (max-width: 768px) {
53 | flex-direction: column;
54 | }
55 | `;
56 |
57 | export const ButtonWrapper = styled.div`
58 | display: flex;
59 | justify-content: flex-end;
60 | `;
61 |
62 | export const CoursesWrapper = styled.div`
63 | max-width: 300px;
64 | width: 100%;
65 | `;
66 | export const UserCoursesListTitle = styled.div`
67 | color: ${LIGHT_TEXT_COLOR};
68 | margin-bottom: 10px;
69 | ${GeneralAdaptiveFont}
70 | `;
71 |
72 | export const UserCourseListItem = styled.div`
73 | padding: 8px 15px;
74 | margin-bottom: 20px;
75 | border-radius: 10px;
76 | background-color: ${BG_COLOR};
77 | color: ${DARK_TEXT_COLOR};
78 | `;
79 |
80 | export const CommonWrapper = styled.div`
81 | position: absolute;
82 | top: 0;
83 | display: flex;
84 | flex-direction: column;
85 | justify-content: center;
86 | align-items: center;
87 | width: 100%;
88 | `;
89 |
--------------------------------------------------------------------------------
/src/modules/LoginPage/components/LoginInfoBlock/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 | import {
3 | StyledLoginInfoBlock,
4 | StyledLoginTitle,
5 | StyledLoginRegistrationLink,
6 | StyledLoginTextWrapper,
7 | } from './styled';
8 | import { AUTH_BACKEND_LINK } from 'appConstants';
9 | import { useTranslation } from 'react-i18next';
10 |
11 | export const LoginInfoBlock: FC = () => {
12 | const { t } = useTranslation();
13 | return (
14 |
15 | {t('Sign in')}
16 |
17 | {t('Sign in with Github')}
18 |
19 |
20 | {t('Don’t have github account?')}
21 | {t('Sign up')}
22 |
23 |
24 | );
25 | };
26 |
--------------------------------------------------------------------------------
/src/modules/LoginPage/components/LoginInfoBlock/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { WHITE_COLOR, DARK_TEXT_COLOR, MAIN1_COLOR } from 'appConstants/colors';
3 | import { GeneralAdaptiveFont, GeneralButtonPadding, H1AdaptiveFont } from 'typography';
4 |
5 | export const StyledLoginInfoBlock = styled.div`
6 | display: flex;
7 | flex-direction: column;
8 | justify-content: center;
9 | align-items: center;
10 | margin-left: 12%;
11 | padding: 20px;
12 | font-size: 1rem;
13 | color: ${DARK_TEXT_COLOR};
14 | background-color: ${WHITE_COLOR};
15 | border-radius: 20px;
16 | gap: 30px;
17 | ${GeneralAdaptiveFont};
18 |
19 | @media (max-width: 992px) {
20 | margin: 0 auto;
21 | }
22 | @media (max-width: 768px) {
23 | padding: 20px 15px;
24 | gap: 25px;
25 | }
26 | @media (max-width: 550px) {
27 | gap: 20px;
28 | }
29 | @media (max-width: 440px) {
30 | gap: 15px;
31 | }
32 | `;
33 |
34 | export const StyledLoginTitle = styled.h2`
35 | margin: 0;
36 | font-weight: 600;
37 | ${H1AdaptiveFont};
38 | `;
39 |
40 | export const StyledLoginRegistrationLink = styled.a`
41 | display: inline-block;
42 | margin-top: 10px;
43 | text-align: center;
44 | color: ${WHITE_COLOR};
45 | text-decoration: none;
46 | background-color: ${MAIN1_COLOR};
47 | border-radius: 20px;
48 | ${GeneralButtonPadding}
49 | `;
50 |
51 | export const StyledLoginTextWrapper = styled.div`
52 | display: flex;
53 | font-weight: normal;
54 | line-height: 150%;
55 | gap: 10px;
56 |
57 | p {
58 | margin: 0;
59 | }
60 |
61 | a {
62 | font-weight: 500;
63 | color: ${MAIN1_COLOR};
64 | text-decoration: none;
65 | }
66 | `;
67 |
--------------------------------------------------------------------------------
/src/modules/LoginPage/components/index.ts:
--------------------------------------------------------------------------------
1 | export { LoginInfoBlock } from './LoginInfoBlock';
2 |
--------------------------------------------------------------------------------
/src/modules/LoginPage/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 | import { useSelector } from 'react-redux';
3 | import { Redirect } from 'react-router-dom';
4 | import { selectToken } from './selectors';
5 | import { StyledLoginImage, StyledLoginPage, StyledLoginPageItemsWrapper } from './styled';
6 | import { LoginInfoBlock } from './components';
7 |
8 | export const LoginPage: FC = () => {
9 | const loginToken = useSelector(selectToken);
10 |
11 | if (loginToken) return ;
12 |
13 | return (
14 |
15 |
16 |
17 |
18 |
19 |
20 | );
21 | };
22 |
--------------------------------------------------------------------------------
/src/modules/LoginPage/loginPageMiddleware.ts:
--------------------------------------------------------------------------------
1 | import { CURRENT_LANG, CURRENT_COURSE, TOUR_OPENING } from 'appConstants';
2 | import { Course } from 'types';
3 | import { setCurrCourse, setCurrLang } from './loginPageReducer';
4 |
5 | export const setLanguage = (currentLanguage: string) => {
6 | return (dispatch: (actionCreator: any) => void) => {
7 | localStorage.setItem(CURRENT_LANG, currentLanguage);
8 | dispatch(setCurrLang(currentLanguage));
9 | };
10 | };
11 |
12 | export const setCourse = (currentCourse: Course) => {
13 | return (dispatch: (actionCreator: any) => void) => {
14 | localStorage.setItem(CURRENT_COURSE, JSON.stringify(currentCourse));
15 | dispatch(setCurrCourse(currentCourse));
16 | };
17 | };
18 |
19 | export const setTourOpening = (tourOpening: string) => {
20 | return () => {
21 | localStorage.setItem(TOUR_OPENING, tourOpening);
22 | };
23 | };
24 |
--------------------------------------------------------------------------------
/src/modules/LoginPage/loginPageReducer.ts:
--------------------------------------------------------------------------------
1 | import {
2 | DEFAULT_LANGUAGE,
3 | SET_COMMON_ERROR,
4 | SET_CURR_COURSE,
5 | SET_CURR_LANG,
6 | SET_TOKEN,
7 | SET_BURGER_MENU_OPEN,
8 | SET_EDIT_PROFILE_DATA_CHANGE,
9 | SET_PATH_TO_THE_PAGE,
10 | SET_IS_TOUR_OPEN,
11 | } from 'appConstants';
12 | import { createActions, handleActions } from 'redux-actions';
13 | import { StateLoginPage } from 'types';
14 |
15 | export const loginPageState = {
16 | loginToken: null,
17 | currCourse: {
18 | id: '',
19 | name: '',
20 | teamSize: null,
21 | isActive: true,
22 | },
23 | currLanguage: DEFAULT_LANGUAGE,
24 | isCommonError: false,
25 | isBurgerMenuOpen: false,
26 | isEditProfileDataChange: false,
27 | pathToThePage: '',
28 | isTourOpen: false,
29 | };
30 |
31 | export const {
32 | setToken,
33 | setCurrCourse,
34 | setCurrLang,
35 | setCommonError,
36 | setBurgerMenuOpen,
37 | setEditProfileDataChange,
38 | setPathToThePage,
39 | setIsTourOpen,
40 | } = createActions({
41 | SET_TOKEN: (loginToken) => ({ loginToken }),
42 | SET_CURR_COURSE: (currCourse) => ({ currCourse }),
43 | SET_CURR_LANG: (currLanguage) => ({ currLanguage }),
44 | SET_COMMON_ERROR: (isCommonError) => ({ isCommonError }),
45 | SET_BURGER_MENU_OPEN: (isBurgerMenuOpen) => ({ isBurgerMenuOpen }),
46 | SET_EDIT_PROFILE_DATA_CHANGE: (isEditProfileDataChange) => ({
47 | isEditProfileDataChange,
48 | }),
49 | SET_PATH_TO_THE_PAGE: (pathToThePage) => ({
50 | pathToThePage,
51 | }),
52 | SET_IS_TOUR_OPEN: (isTourOpen) => ({
53 | isTourOpen,
54 | }),
55 | });
56 |
57 | export const loginPageReducer = handleActions(
58 | {
59 | [SET_TOKEN]: (state, { payload: { loginToken } }) => ({
60 | ...state,
61 | loginToken,
62 | }),
63 | [SET_CURR_COURSE]: (state, { payload: { currCourse } }) => ({
64 | ...state,
65 | currCourse,
66 | }),
67 | [SET_CURR_LANG]: (state, { payload: { currLanguage } }) => ({
68 | ...state,
69 | currLanguage,
70 | }),
71 | [SET_COMMON_ERROR]: (state, { payload: { isCommonError } }) => ({
72 | ...state,
73 | isCommonError,
74 | }),
75 | [SET_BURGER_MENU_OPEN]: (state, { payload: { isBurgerMenuOpen } }) => ({
76 | ...state,
77 | isBurgerMenuOpen,
78 | }),
79 | [SET_EDIT_PROFILE_DATA_CHANGE]: (state, { payload: { isEditProfileDataChange } }) => ({
80 | ...state,
81 | isEditProfileDataChange,
82 | }),
83 | [SET_PATH_TO_THE_PAGE]: (state, { payload: { pathToThePage } }) => ({
84 | ...state,
85 | pathToThePage,
86 | }),
87 | [SET_IS_TOUR_OPEN]: (state, { payload: { isTourOpen } }) => ({
88 | ...state,
89 | isTourOpen,
90 | }),
91 | },
92 | loginPageState
93 | );
94 |
--------------------------------------------------------------------------------
/src/modules/LoginPage/selectors.ts:
--------------------------------------------------------------------------------
1 | import { State } from 'types';
2 |
3 | export const selectToken = (state: State) => state.loginPageReducer.loginToken;
4 |
5 | export const selectCurrCourse = (state: State) => state.loginPageReducer.currCourse;
6 |
7 | export const selectCurrLanguage = (state: State) => state.loginPageReducer.currLanguage;
8 |
9 | export const selectIsCommonError = (state: State) => state.loginPageReducer.isCommonError;
10 |
11 | export const selectIsBurgerMenuOpen = (state: State) => state.loginPageReducer.isBurgerMenuOpen;
12 |
13 | export const selectIsEditProfileDataChange = (state: State) =>
14 | state.loginPageReducer.isEditProfileDataChange;
15 |
16 | export const selectPathToThePage = (state: State) => state.loginPageReducer.pathToThePage;
17 |
18 | export const selectIsTourOpen = (state: State) => state.loginPageReducer.isTourOpen;
19 |
--------------------------------------------------------------------------------
/src/modules/LoginPage/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { MAIN1_COLOR, WHITE_COLOR } from 'appConstants/colors';
3 | import { ReactComponent as LoginImage } from 'assets/svg/loginImage.svg';
4 |
5 | export const StyledLoginPage = styled.div`
6 | position: fixed;
7 | top: 0;
8 | right: 0;
9 | bottom: 0;
10 | left: 0;
11 | display: flex;
12 | justify-content: center;
13 | align-items: center;
14 | width: 100%;
15 | height: 100vh;
16 | margin: 0 auto;
17 | background: linear-gradient(90deg, ${WHITE_COLOR} 75%, ${MAIN1_COLOR} 75%);
18 | `;
19 |
20 | export const StyledLoginPageItemsWrapper = styled.div`
21 | position: relative;
22 | display: flex;
23 | align-items: center;
24 | width: 100%;
25 | max-width: 1440px;
26 | height: 100%;
27 | overflow: hidden;
28 | `;
29 |
30 | export const StyledLoginImage = styled(LoginImage)`
31 | position: absolute;
32 | top: 0;
33 | right: 0;
34 | z-index: -1;
35 | width: auto;
36 | height: 100%;
37 |
38 | @media (max-width: 991px) {
39 | left: 50%;
40 | transform: translate(-50%, 0%);
41 | }
42 | `;
43 |
--------------------------------------------------------------------------------
/src/modules/NotFoundPage/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 | import { useTranslation } from 'react-i18next';
3 | import { ContentPageWrapper, PageTitle } from 'typography';
4 | import { NotFoundPageWrapper } from './styled';
5 |
6 | export const NotFoundPage: FC = () => {
7 | const { t } = useTranslation();
8 | return (
9 |
10 |
11 | {t('Not found!')}
12 |
13 |
14 | );
15 | };
16 |
--------------------------------------------------------------------------------
/src/modules/NotFoundPage/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const NotFoundPageWrapper = styled.div`
4 | display: flex;
5 | justify-content: center;
6 | align-items: center;
7 | width: 100%;
8 | height: 100%;
9 | `;
10 |
--------------------------------------------------------------------------------
/src/modules/StudentsTable/components/Dashboard/components/TableBody/components/TableRow/components/TableItem/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, MouseEvent, useState } from 'react';
2 | import { TablePopup } from 'components/TablePopup';
3 | import { StyledTableItem } from './styled';
4 |
5 | type TableItemProps = {
6 | item: string;
7 | index: number;
8 | dataLength: number;
9 | };
10 |
11 | export const TableItem: FC = ({ item, index, dataLength }) => {
12 | const [tableItemCursor, setTableItemCursor] = useState(false);
13 | const [showPopup, setShowPopup] = useState(false);
14 | const [popupElements, setPopupElements] = useState([]);
15 |
16 | const mouseOverHandler = (event: MouseEvent) => {
17 | const target = event.target as HTMLDivElement;
18 | if (target.scrollWidth !== target.clientWidth) {
19 | setShowPopup(true);
20 | setPopupElements(target?.textContent?.split(',') as string[]);
21 | setTableItemCursor(true);
22 | }
23 | };
24 |
25 | const mouseLeaveHandler = () => {
26 | setShowPopup(false);
27 | setPopupElements([]);
28 | setTableItemCursor(false);
29 | };
30 |
31 | return (
32 |
38 | {item}
39 | {showPopup && }
40 |
41 | );
42 | };
43 |
--------------------------------------------------------------------------------
/src/modules/StudentsTable/components/Dashboard/components/TableBody/components/TableRow/components/TableItem/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | type TStyledTableItem = {
4 | tableItemCursor: boolean;
5 | };
6 |
7 | export const StyledTableItem = styled.div`
8 | position: relative;
9 | max-width: 140px;
10 | cursor: ${({ tableItemCursor }) => (tableItemCursor ? 'pointer' : 'unset')};
11 | .TableItem__first-element {
12 | width: 100%;
13 | margin: 0;
14 | overflow: hidden;
15 | white-space: nowrap;
16 | text-overflow: ellipsis;
17 | }
18 | `;
19 |
--------------------------------------------------------------------------------
/src/modules/StudentsTable/components/Dashboard/components/TableBody/components/TableRow/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 | import { ListChildComponentProps } from 'react-window';
3 | import { TableItem } from './components/TableItem';
4 | import { StyledTableRow } from './styled';
5 |
6 | export const TableRow: FC = ({ data, index, style }) => {
7 | const optimalItemIndexCount = 2;
8 | return (
9 |
10 | {data[index].map((item: string, ind: number) => (
11 | optimalItemIndexCount ? index / (data.length - 1) : 0}
15 | key={`TableItemKey-${ind}`}
16 | />
17 | ))}
18 |
19 | );
20 | };
21 |
--------------------------------------------------------------------------------
/src/modules/StudentsTable/components/Dashboard/components/TableBody/components/TableRow/styled.ts:
--------------------------------------------------------------------------------
1 | import { BG_COLOR, DARK_TEXT_COLOR } from 'appConstants/colors';
2 | import styled from 'styled-components';
3 |
4 | export const StyledTableRow = styled.div`
5 | display: flex;
6 | justify-content: space-between;
7 | align-items: center;
8 | padding: 20px;
9 | font-weight: 400;
10 | line-height: 150%;
11 | color: ${DARK_TEXT_COLOR};
12 | border-radius: 10px;
13 | &:nth-child(2n) {
14 | background-color: ${BG_COLOR};
15 | }
16 |
17 | @media (max-width: 768px) {
18 | padding: 10px;
19 | }
20 | @media (max-width: 440px) {
21 | padding: 5px;
22 | }
23 | `;
24 |
--------------------------------------------------------------------------------
/src/modules/StudentsTable/components/Dashboard/components/TableBody/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, useMemo, ReactText } from 'react';
2 | import { useSelector } from 'react-redux';
3 | import { FixedSizeList as List } from 'react-window';
4 | import AutoSizer from 'react-virtualized-auto-sizer';
5 | import { StyledTableBody } from './styled';
6 | import './styles.css';
7 | import { User, Course, Team } from 'types';
8 | import { USERS_PER_PAGE } from 'appConstants';
9 | import { TableRow } from './components/TableRow';
10 | import { selectCurrCourse } from 'modules/LoginPage/selectors';
11 | import { useTranslation } from 'react-i18next';
12 |
13 | type TableBodyProps = {
14 | users: User[];
15 | page: number;
16 | };
17 |
18 | export const TableBody: FC = ({ users, page }) => {
19 | const currCourse = useSelector(selectCurrCourse);
20 | const { t } = useTranslation();
21 |
22 | const usersData: Array = useMemo(
23 | () =>
24 | users.map((user: User, index: number) => {
25 | return [
26 | `${index + 1 + page * USERS_PER_PAGE}`,
27 | `${user.firstName} ${user.lastName || null}`,
28 | `${user.score}`,
29 | user.teams.find((team: Team) => team.courseId === currCourse.id)
30 | ? `${user.teams.find((team: Team) => team.courseId === currCourse.id)?.number}`
31 | : (t('No team yet.') as string),
32 | user.telegram || `${t('No')} telegram.`,
33 | user.discord || `${t('No')} discord.`,
34 | user.github || `${t('No')} GitHub.`,
35 | `${user.country},
36 | ${user.city}`,
37 | user.courses.length
38 | ? user.courses.map((course: Course) => course.name).join(', ')
39 | : (t('No courses.') as string),
40 | ];
41 | }),
42 | [users, page, currCourse.id, t]
43 | );
44 |
45 | return (
46 |
47 |
48 | {({ height, width }) => (
49 |
57 | {TableRow}
58 |
59 | )}
60 |
61 |
62 | );
63 | };
64 |
--------------------------------------------------------------------------------
/src/modules/StudentsTable/components/Dashboard/components/TableBody/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const StyledTableBody = styled.div`
4 | display: flex;
5 | width: 100%;
6 | height: 100%;
7 | `;
8 |
--------------------------------------------------------------------------------
/src/modules/StudentsTable/components/Dashboard/components/TableHead/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 | import { TABLE_HEADERS } from 'appConstants';
3 | import { StyledTableHead, StyledTableHeadRow, StyledTableHeader } from './styled';
4 | import { useTranslation } from 'react-i18next';
5 |
6 | export const TableHead: FC = () => {
7 | const { t } = useTranslation();
8 | return (
9 |
10 |
11 | {TABLE_HEADERS.map((tableHeader: string, index: number) => (
12 |
13 | {t(tableHeader)}
14 |
15 | ))}
16 |
17 |
18 | );
19 | };
20 |
--------------------------------------------------------------------------------
/src/modules/StudentsTable/components/Dashboard/components/TableHead/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { LIGHT_TEXT_COLOR, DASHBOARD_HEADER_BG_COLOR } from 'appConstants/colors';
3 |
4 | export const StyledTableHead = styled.div`
5 | display: flex;
6 | flex-direction: column;
7 | justify-content: center;
8 | align-items: center;
9 | margin-right: 10px;
10 | padding: 10px 20px;
11 | color: ${LIGHT_TEXT_COLOR};
12 | background-color: ${DASHBOARD_HEADER_BG_COLOR};
13 | border-radius: 10px;
14 | @media (max-width: 768px) {
15 | padding: 10px;
16 | }
17 | @media (max-width: 440px) {
18 | padding: 5px;
19 | }
20 | `;
21 |
22 | export const StyledTableHeadRow = styled.div`
23 | display: flex;
24 | justify-content: space-between;
25 | align-items: center;
26 | width: 100%;
27 | `;
28 |
29 | export const StyledTableHeader = styled.div`
30 | max-width: 140px;
31 | height: auto;
32 | margin: 0;
33 | font-weight: 600;
34 | line-height: 150%;
35 | text-align: start;
36 | `;
37 |
--------------------------------------------------------------------------------
/src/modules/StudentsTable/components/Dashboard/components/index.ts:
--------------------------------------------------------------------------------
1 | export { TableBody } from './TableBody';
2 | export { TableHead } from './TableHead';
3 |
--------------------------------------------------------------------------------
/src/modules/StudentsTable/components/Dashboard/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 | import { TableBody, TableHead } from './components';
3 | import { StyledTable } from './styled';
4 | import { User } from 'types';
5 |
6 | type DashboardProps = {
7 | users: User[];
8 | page: number;
9 | };
10 |
11 | export const Dashboard: FC = ({ users, page }) => {
12 | return (
13 |
14 |
15 |
16 |
17 | );
18 | };
19 |
--------------------------------------------------------------------------------
/src/modules/StudentsTable/components/Dashboard/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { WHITE_COLOR } from 'appConstants/colors';
3 | import { GeneralAdaptiveFont } from 'typography';
4 |
5 | export const StyledTable = styled.div`
6 | display: flex;
7 | flex-direction: column;
8 | width: 100%;
9 | max-width: 1320px;
10 | height: 75vh;
11 | margin: 0 auto;
12 | padding: 10px;
13 | padding-right: 0;
14 | font-size: 1rem;
15 | background-color: ${WHITE_COLOR};
16 | border-radius: 20px;
17 | ${GeneralAdaptiveFont};
18 | `;
19 |
--------------------------------------------------------------------------------
/src/modules/StudentsTable/selectors.ts:
--------------------------------------------------------------------------------
1 | import { State } from 'types';
2 |
3 | export const selectUserData = (state: State) => state.studentsTableReducer.userData;
4 |
5 | export const selectFilterData = (state: State) => state.studentsTableReducer.filterData;
6 |
--------------------------------------------------------------------------------
/src/modules/StudentsTable/studentsTableReducer.ts:
--------------------------------------------------------------------------------
1 | import { SET_USER_DATA, SET_FILTER_DATA } from 'appConstants';
2 | import { StateStudentsTable } from 'types';
3 | import { defaultFilterData } from 'components/FilterForm/filterFormFields';
4 | import { createActions, handleActions } from 'redux-actions';
5 |
6 | export const studentsTableState = {
7 | userData: {
8 | id: '',
9 | firstName: '',
10 | lastName: '',
11 | github: '',
12 | telegram: null,
13 | discord: '',
14 | score: 1000,
15 | country: '',
16 | city: '',
17 | avatar: '',
18 | isAdmin: false,
19 | courses: [],
20 | email: '',
21 | courseIds: [''],
22 | teamIds: [''],
23 | teams: [],
24 | },
25 | filterData: defaultFilterData,
26 | };
27 |
28 | export const { setUserData, setFilterData } = createActions({
29 | SET_USER_DATA: (userData) => ({ userData }),
30 | SET_FILTER_DATA: (filterData) => ({ filterData }),
31 | });
32 |
33 | export const studentsTableReducer = handleActions(
34 | {
35 | [SET_USER_DATA]: (state, { payload: { userData } }) => ({
36 | ...state,
37 | userData,
38 | }),
39 | [SET_FILTER_DATA]: (state, { payload: { filterData } }) => ({
40 | ...state,
41 | filterData,
42 | }),
43 | },
44 | studentsTableState
45 | );
46 |
--------------------------------------------------------------------------------
/src/modules/StudentsTable/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { DARK_TEXT_COLOR } from 'appConstants/colors';
3 | import { ScrollBar } from 'typography';
4 |
5 | export const StudentTableWrapper = styled.div`
6 | display: flex;
7 | flex-direction: column;
8 | align-items: center;
9 | width: 100%;
10 | height: fit-content;
11 | max-width: 1440px;
12 | padding: 0 4% 60px;
13 | overflow-y: scroll;
14 | ${ScrollBar};
15 | @media screen and (max-width: 768px) {
16 | padding-bottom: 50px;
17 | }
18 | `;
19 |
20 | export const TableTitle = styled.h1`
21 | align-self: flex-start;
22 | width: 80%;
23 | margin: 40px 0;
24 | font: 700 30px/45px 'Poppins', sans-serif;
25 | color: ${DARK_TEXT_COLOR};
26 |
27 | @media (max-width: 1200px) {
28 | font-size: 26px;
29 | line-height: 35px;
30 | }
31 | @media (max-width: 992px) {
32 | margin: 35px 0;
33 | font-size: 24px;
34 | line-height: 30px;
35 | }
36 | @media (max-width: 768px) {
37 | margin: 30px 0;
38 | font-size: 22px;
39 | line-height: 25px;
40 | }
41 | @media (max-width: 550px) {
42 | margin: 30px 0;
43 | font-size: 20px;
44 | line-height: 25px;
45 | }
46 | @media (max-width: 440px) {
47 | margin: 20px 0;
48 | font-size: 17px;
49 | line-height: 20px;
50 | }
51 | `;
52 |
--------------------------------------------------------------------------------
/src/modules/TeamsList/components/Teams/components/MemberListToggle/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 | import { MembersListToggleStyled, Chevron } from './styled';
3 | import { ReactComponent as ChevronArrow } from 'assets/svg/chevron-arrow.svg';
4 | import { useTranslation } from 'react-i18next';
5 |
6 | type MembersListToggle = {
7 | countMembers: number;
8 | isOpen: boolean;
9 | onToggleList: () => void;
10 | color?: string;
11 | };
12 |
13 | export const MembersListToggle: FC = ({
14 | countMembers,
15 | isOpen,
16 | onToggleList,
17 | color,
18 | }) => {
19 | const { t } = useTranslation();
20 | return (
21 | {
23 | !!countMembers && onToggleList();
24 | }}
25 | color={color}
26 | >
27 |
28 | {countMembers || 0} {countMembers === 1 ? t('member') : t('members')}
29 |
30 |
31 |
32 |
33 |
34 | );
35 | };
36 |
--------------------------------------------------------------------------------
/src/modules/TeamsList/components/Teams/components/MemberListToggle/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { WHITE_COLOR } from 'appConstants/colors';
3 | import { SVGArrowAdaptive } from 'typography';
4 |
5 | type ChevronProps = {
6 | open: boolean;
7 | };
8 |
9 | export const MembersListToggleStyled = styled.div`
10 | display: flex;
11 | justify-content: space-between;
12 | align-items: center;
13 | width: 134px;
14 | cursor: pointer;
15 | color: ${({ color }) => color || WHITE_COLOR};
16 |
17 | path {
18 | stroke: currentColor;
19 | }
20 |
21 | @media (max-width: 992px) {
22 | width: 115px;
23 | }
24 | @media (max-width: 768px) {
25 | width: 105px;
26 | }
27 | @media (max-width: 440px) {
28 | width: 85px;
29 | }
30 | `;
31 |
32 | export const Chevron = styled.div`
33 | display: flex;
34 | justify-content: center;
35 | align-items: center;
36 | transform: rotateX(${({ open }) => (open ? '180deg' : '0deg')});
37 | margin-left: 10px;
38 | transition: all 0.3s;
39 |
40 | svg {
41 | ${SVGArrowAdaptive};
42 | }
43 | `;
44 |
--------------------------------------------------------------------------------
/src/modules/TeamsList/components/Teams/components/MyTeam/components/MyTeamInfoBlock/components/MyTeamInfoLine/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, MouseEvent, useState } from 'react';
2 | import { InfoLineStyled, CopyClipboardButton } from './styled';
3 | import { TablePopup } from 'components/TablePopup';
4 |
5 | type MyTeamInfoLine = {
6 | value?: string;
7 | hoverHandler?: () => void;
8 | };
9 |
10 | export const MyTeamInfoLine: FC = ({ value }) => {
11 | const [showPopup, setShowPopup] = useState(false);
12 | const [isCopy, setIsCopy] = useState(false);
13 | const copyInfo = (value: string) => {
14 | navigator.clipboard
15 | .writeText(value)
16 | .then(() => {
17 | setIsCopy(true);
18 | setTimeout(() => {
19 | setIsCopy(false);
20 | }, 1000);
21 | })
22 | .catch((err) => {
23 | console.log(err);
24 | });
25 | };
26 |
27 | const mouseOverHandler = (event: MouseEvent) => {
28 | const target = event.target as HTMLDivElement;
29 | if (target.scrollWidth !== target.clientWidth) {
30 | setShowPopup(true);
31 | }
32 | };
33 |
34 | const currValue: string = value || '';
35 | return (
36 |
37 | setShowPopup(false)}
41 | >
42 | {currValue}
43 |
44 | {showPopup && }
45 | copyInfo(currValue)} />
46 |
47 | );
48 | };
49 |
--------------------------------------------------------------------------------
/src/modules/TeamsList/components/Teams/components/MyTeam/components/MyTeamInfoBlock/components/MyTeamInfoLine/styled.tsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { WHITE_COLOR } from 'appConstants/colors';
3 | import { TextBold, GeneralAdaptiveFont, SVGParamsAdaptive } from 'typography';
4 | import { ReactComponent as CopyIcon } from 'assets/svg/copy.svg';
5 |
6 | type TInfoLineStyled = {
7 | blink: boolean;
8 | };
9 |
10 | export const InfoLineStyled = styled.div`
11 | ${TextBold};
12 | color: ${WHITE_COLOR};
13 | display: flex;
14 | align-items: center;
15 | border-radius: 5px;
16 |
17 | .info__text {
18 | overflow: hidden;
19 | white-space: nowrap;
20 | text-overflow: ellipsis;
21 | animation: ${({ blink }) => (blink ? 'blink 1s' : 'none')};
22 | border-radius: 3px;
23 | padding: 0 7px;
24 | margin-left: -7px;
25 | ${GeneralAdaptiveFont};
26 | }
27 |
28 | @keyframes blink {
29 | from {
30 | background: rgb(101, 80, 246, 0.5);
31 | }
32 | to {
33 | background: rgba(101, 80, 246, 0);
34 | }
35 | }
36 | `;
37 |
38 | export const CopyClipboardButton = styled(CopyIcon)`
39 | cursor: pointer;
40 | margin-left: 3px;
41 | ${SVGParamsAdaptive};
42 | `;
43 |
--------------------------------------------------------------------------------
/src/modules/TeamsList/components/Teams/components/MyTeam/components/MyTeamInfoBlock/components/NotificationPopup/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 | import { NotificationPopupStyled } from './styled';
3 |
4 | const NotificationPopup: FC = ({ children }) => {
5 | return {children};
6 | };
7 |
8 | export default NotificationPopup;
9 |
--------------------------------------------------------------------------------
/src/modules/TeamsList/components/Teams/components/MyTeam/components/MyTeamInfoBlock/components/NotificationPopup/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { LIGHT_TEXT_COLOR, WHITE_COLOR } from 'appConstants/colors';
3 |
4 | export const NotificationPopupStyled = styled.div`
5 | position: absolute;
6 | top: 153%;
7 | right: 0;
8 | width: 240px;
9 | background-color: ${WHITE_COLOR};
10 | color: ${LIGHT_TEXT_COLOR};
11 | border-radius: 10px;
12 | padding: 20px 20px;
13 | z-index: 1;
14 |
15 | &::after {
16 | content: '';
17 | position: absolute;
18 | top: 0;
19 | right: 8px;
20 | transform: rotateZ(59deg) skew(33deg);
21 | height: 22px;
22 | width: 22px;
23 | border-radius: 4px;
24 | background-color: ${WHITE_COLOR};
25 | }
26 | `;
27 |
--------------------------------------------------------------------------------
/src/modules/TeamsList/components/Teams/components/MyTeam/components/MyTeamInfoBlock/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, useState } from 'react';
2 | import { useDispatch } from 'react-redux';
3 | import { StyledMyTeamInfoBlock, InfoButton } from './styled';
4 | import { MyTeamInfoLine } from './components/MyTeamInfoLine';
5 | import { ReactComponent as InfoIcon } from 'assets/svg/info.svg';
6 | import { ReactComponent as EditIcon } from 'assets/svg/edit.svg';
7 | import NotificationPopup from './components/NotificationPopup';
8 | import { useTranslation } from 'react-i18next';
9 | import { activeModalUpdateSocialLink } from 'modules/TeamsList/teamsListReducer';
10 |
11 | type MyTeamInfoBlockProps = {
12 | title: string;
13 | icon: 'info' | 'edit';
14 | value: string;
15 | };
16 |
17 | export const MyTeamInfoBlock: FC = ({ title, icon, value }) => {
18 | const dispatch = useDispatch();
19 | const [hover, setHover] = useState(false);
20 | const { t } = useTranslation();
21 | return (
22 |
23 | {t(title)}
24 |
25 | {icon === 'info' ? (
26 | setHover(true)} onMouseOut={() => setHover(false)}>
27 |
28 | {hover && (
29 | {t('The password is required to join the team.')}
30 | )}
31 |
32 | ) : (
33 | dispatch(activeModalUpdateSocialLink(true))}>
34 |
35 |
36 | )}
37 |
38 | );
39 | };
40 |
--------------------------------------------------------------------------------
/src/modules/TeamsList/components/Teams/components/MyTeam/components/MyTeamInfoBlock/styled.ts:
--------------------------------------------------------------------------------
1 | import styled, { css } from 'styled-components';
2 | import { WHITE_COLOR, MAIN2_LIGHT_COLOR, MAIN2_COLOR } from 'appConstants/colors';
3 | import { TextRegular, GeneralAdaptiveFont, SVGParamsAdaptive } from 'typography';
4 |
5 | export const StyledMyTeamInfoBlock = styled.div`
6 | position: relative;
7 | max-width: 300px;
8 | width: 100%;
9 | border-radius: 10px;
10 | padding: 20px;
11 | color: ${WHITE_COLOR};
12 | background-color: ${MAIN2_LIGHT_COLOR};
13 |
14 | .infoBlock__title {
15 | ${TextRegular};
16 | color: ${WHITE_COLOR};
17 | margin-bottom: 10px;
18 | ${GeneralAdaptiveFont};
19 | }
20 |
21 | @media (max-width: 992px) {
22 | max-width: 50%;
23 | }
24 | @media (max-width: 580px) {
25 | max-width: 100%;
26 | }
27 | `;
28 |
29 | const InfoBlockButton = css`
30 | position: absolute;
31 | top: 10px;
32 | right: 10px;
33 | display: flex;
34 | align-items: center;
35 | justify-content: center;
36 | width: 32px;
37 | height: 32px;
38 | padding: 8px;
39 | background: ${MAIN2_COLOR};
40 | fill: ${WHITE_COLOR};
41 | cursor: pointer;
42 | border-radius: 5px;
43 | `;
44 |
45 | export const InfoButton = styled.div`
46 | ${InfoBlockButton};
47 |
48 | svg {
49 | ${SVGParamsAdaptive};
50 | }
51 | `;
52 |
--------------------------------------------------------------------------------
/src/modules/TeamsList/components/Teams/components/MyTeam/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, useState } from 'react';
2 | import { useDispatch } from 'react-redux';
3 |
4 | import { StyledMyTeam, HeaderDecor, TableWrapper } from './styled';
5 | import { TeamButton } from 'typography';
6 | import { DARK_TEXT_COLOR, WHITE_COLOR } from 'appConstants/colors';
7 | import { Team } from 'types';
8 | import { MembersListToggle } from '../MemberListToggle';
9 | import { MyTeamInfoBlock } from './components/MyTeamInfoBlock';
10 | import { TeamUserTable } from '../TeamUserTable';
11 | import { useTranslation } from 'react-i18next';
12 | import { activeModalLeave, activeModalRemoveCourse } from 'modules/TeamsList/teamsListReducer';
13 |
14 | type MyTeamProps = {
15 | team: Team;
16 | userId: string;
17 | };
18 |
19 | export const MyTeam: FC = ({ team, userId }) => {
20 | const [isOpen, setOpenState] = useState(false);
21 | const { t } = useTranslation();
22 |
23 | const dispatch = useDispatch();
24 | const toggleListHandler = () => setOpenState(!isOpen);
25 | const leaveTeam = () => dispatch(activeModalLeave(true));
26 | const removeCourse = () => dispatch(activeModalRemoveCourse(true));
27 |
28 | const countMember = team?.members?.length;
29 | return (
30 |
31 |
32 |
33 | {t('My team - Team')} {team.number}
34 |
35 |
36 |
37 |
38 |
39 |
40 |
46 | {t('Leave course')}
47 |
48 |
49 |
50 |
51 | {t('Leave team')}
52 |
53 |
54 |
55 |
60 |
61 |
62 |
63 |
64 | {isOpen && }
65 |
66 |
67 | );
68 | };
69 |
--------------------------------------------------------------------------------
/src/modules/TeamsList/components/Teams/components/TeamItem/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, useState } from 'react';
2 | import { TeamItemStyled, TeamItemTableWrapper } from './styled';
3 | import { User } from 'types';
4 | import { MembersListToggle } from '../MemberListToggle';
5 | import { LIGHT_TEXT_COLOR } from 'appConstants/colors';
6 | import { TeamUserTable } from '../TeamUserTable';
7 |
8 | type TeamItemProps = {
9 | name: string;
10 | description?: string;
11 | countMember: number;
12 | members: User[];
13 | };
14 |
15 | export const TeamItem: FC = ({ name, countMember, description, members }) => {
16 | const [isOpen, setToggle] = useState(false);
17 | const toggleListHandler = () => {
18 | setToggle(!isOpen);
19 | };
20 | return (
21 |
22 |
23 |
{name}
24 | {description &&
{description}
}
25 |
31 |
32 |
33 | {isOpen && }
34 |
35 |
36 | );
37 | };
38 |
--------------------------------------------------------------------------------
/src/modules/TeamsList/components/Teams/components/TeamItem/styled.tsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { TextRegular, TextSemiBold, GeneralAdaptiveFont } from 'typography';
3 | import { WHITE_COLOR } from 'appConstants/colors';
4 | import { TableWrapper } from '../MyTeam/styled';
5 |
6 | export const TeamItemStyled = styled.div`
7 | background: ${WHITE_COLOR};
8 | border-radius: 20px;
9 | margin-bottom: 20px;
10 |
11 | .teamItem__header {
12 | padding: 20px 30px;
13 | display: flex;
14 | align-items: center;
15 | justify-content: space-between;
16 | ${GeneralAdaptiveFont};
17 | }
18 |
19 | .teamItem__name {
20 | ${TextSemiBold};
21 | ${GeneralAdaptiveFont};
22 | }
23 | .teamItem__description {
24 | ${TextRegular};
25 | flex-grow: 1;
26 | }
27 | `;
28 |
29 | export const TeamItemTableWrapper = styled(TableWrapper)`
30 | margin-top: ${({ open }) => (open ? '-20px' : '0')};
31 | ${({ open }) => (open ? 'padding: 20px 30px 20px' : null)};
32 |
33 | @media (max-width: 440px) {
34 | ${({ open }) => (open ? 'padding: 20px 15px 20px' : null)};
35 | }
36 | `;
37 |
--------------------------------------------------------------------------------
/src/modules/TeamsList/components/Teams/components/TeamUserTable/components/TableRow/components/ExpelButton/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 | import { SmallCrossIcon, StyledExpelButton } from './styled';
3 |
4 | type ExpelButtonProps = {
5 | onClickHandler?: () => void;
6 | };
7 |
8 | export const ExpelButton: FC = ({ onClickHandler }) => {
9 | return (
10 |
11 |
12 | Expel
13 |
14 | );
15 | };
16 |
--------------------------------------------------------------------------------
/src/modules/TeamsList/components/Teams/components/TeamUserTable/components/TableRow/components/ExpelButton/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { ReactComponent as Cross } from 'assets/svg/cross-small.svg';
3 | import { MAIN1_COLOR } from 'appConstants/colors';
4 | import { SVGArrowAdaptive } from 'typography';
5 |
6 | type StyledExpelButtonProps = {
7 | color?: string;
8 | };
9 |
10 | export const StyledExpelButton = styled.div`
11 | width: 63px;
12 | display: flex;
13 | align-items: center;
14 | justify-content: space-between;
15 | cursor: pointer;
16 | color: ${({ color }) => (color ? color : MAIN1_COLOR)};
17 |
18 | @media (max-width: 550px) {
19 | width: 55px;
20 | }
21 | `;
22 |
23 | export const SmallCrossIcon = styled(Cross)`
24 | width: 12px;
25 | height: 12px;
26 | ${SVGArrowAdaptive};
27 |
28 | @media (max-width: 992px) and (min-width: 768px) {
29 | width: 11px;
30 | height: 11px;
31 | }
32 |
33 | path {
34 | stroke: currentColor;
35 | }
36 | `;
37 |
--------------------------------------------------------------------------------
/src/modules/TeamsList/components/Teams/components/TeamUserTable/components/TableRow/components/TableCell/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 |
3 | type TTableCell = {
4 | value: any;
5 | isSocialLink?: boolean;
6 | };
7 |
8 | const formatSocialLinks = (link: string | null): string =>
9 | link ? '@' + link.replace('@', '') : '';
10 |
11 | export const TableCell: FC = ({ value, isSocialLink = false }) => {
12 | return {isSocialLink ? formatSocialLinks(value) : value} | ;
13 | };
14 |
--------------------------------------------------------------------------------
/src/modules/TeamsList/components/Teams/components/TeamUserTable/components/TableRow/components/index.ts:
--------------------------------------------------------------------------------
1 | export { TableCell } from './TableCell';
2 | export { ExpelButton } from './ExpelButton';
3 |
--------------------------------------------------------------------------------
/src/modules/TeamsList/components/Teams/components/TeamUserTable/components/TableRow/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 | import { User } from 'types';
3 | import { TableCell } from './components';
4 | import { ExpelButton } from './components';
5 | import { useDispatch } from 'react-redux';
6 | import { activeModalExpel, setTeamMemberExpelId } from 'modules/TeamsList/teamsListReducer';
7 |
8 | type TableRowProps = {
9 | member: User;
10 | count: number;
11 | isMyTeam?: boolean;
12 | userId?: string;
13 | secondTable?: boolean;
14 | };
15 |
16 | export const TableRow: FC = ({
17 | member,
18 | count,
19 | isMyTeam,
20 | userId,
21 | secondTable = false,
22 | }) => {
23 | const { firstName, lastName, score, telegram, discord, github, country, city, id } = member;
24 | const dispatch = useDispatch();
25 | return (
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | {isMyTeam && (
35 | {
40 | dispatch(activeModalExpel(true));
41 | dispatch(setTeamMemberExpelId(id));
42 | }}
43 | />
44 | )
45 | }
46 | />
47 | )}
48 |
49 | );
50 | };
51 |
--------------------------------------------------------------------------------
/src/modules/TeamsList/components/Teams/components/TeamUserTable/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 | import { StyledTeamUserTable } from './styled';
3 | import { User } from 'types';
4 | import { TableRow } from './components/TableRow';
5 | import { TABLE_TEAMS_HEADERS } from 'appConstants';
6 | import { useTranslation } from 'react-i18next';
7 |
8 | type TeamUserTableProps = {
9 | members?: User[];
10 | isMyTeam?: boolean;
11 | userId?: string;
12 | secondTable?: boolean;
13 | };
14 |
15 | export const TeamUserTable: FC = ({
16 | members,
17 | isMyTeam,
18 | userId,
19 | secondTable = false,
20 | }) => {
21 | const { t } = useTranslation();
22 | return (
23 |
24 |
25 |
26 | {TABLE_TEAMS_HEADERS.map((columnName: string) => {
27 | return !isMyTeam && columnName === 'Action' ? null : (
28 | {t(columnName)} |
29 | );
30 | })}
31 |
32 |
33 |
34 | {members &&
35 | members.map((member: User, index: number) => {
36 | return (
37 |
42 | );
43 | })}
44 |
45 |
46 | );
47 | };
48 |
--------------------------------------------------------------------------------
/src/modules/TeamsList/components/Teams/components/TeamUserTable/styled.tsx:
--------------------------------------------------------------------------------
1 | import styled, { css } from 'styled-components';
2 | import {
3 | DASHBOARD_HEADER_BG_COLOR,
4 | LIGHT_TEXT_COLOR,
5 | DARK_TEXT_COLOR,
6 | BG_COLOR,
7 | } from 'appConstants/colors';
8 | import { TextBold, GeneralAdaptiveFont } from 'typography';
9 |
10 | const AdaptiveFirstCellBorderRadius = css`
11 | border-bottom-left-radius: 10px;
12 | border-top-left-radius: 10px;
13 | `;
14 |
15 | const AdaptiveLastCellBorderRadius = css`
16 | border-bottom-right-radius: 10px;
17 | border-top-right-radius: 10px;
18 | `;
19 |
20 | const AdaptiveCellVisible = css`
21 | &:first-child {
22 | ${AdaptiveFirstCellBorderRadius}
23 | }
24 |
25 | &:last-child {
26 | ${AdaptiveLastCellBorderRadius}
27 | }
28 |
29 | @media (max-width: 992px) {
30 | &:nth-child(5),
31 | &:nth-child(6) {
32 | display: none;
33 | }
34 | }
35 | @media (max-width: 650px) {
36 | &:nth-child(7) {
37 | display: none;
38 | }
39 | }
40 | @media (max-width: 500px) {
41 | &:nth-child(3) {
42 | display: none;
43 | }
44 | }
45 | `;
46 |
47 | export const StyledTeamUserTable = styled.table`
48 | width: 100%;
49 | border-collapse: collapse;
50 | transition: all 0.3s ease-in-out;
51 | ${GeneralAdaptiveFont}
52 |
53 | thead {
54 | background-color: ${DASHBOARD_HEADER_BG_COLOR};
55 | color: ${LIGHT_TEXT_COLOR};
56 |
57 | & .SecondTable th:nth-child(4) {
58 | @media (max-width: 650px) {
59 | ${AdaptiveLastCellBorderRadius}
60 | }
61 | }
62 |
63 | & .FirstTable th:nth-child(4) {
64 | @media (max-width: 440px) {
65 | display: none;
66 | }
67 | }
68 |
69 | th {
70 | padding: 10px;
71 | text-align: left;
72 | ${AdaptiveCellVisible}
73 | }
74 | }
75 |
76 | tr.FirstTable td:nth-child(4) {
77 | @media (max-width: 440px) {
78 | display: none;
79 | }
80 | }
81 |
82 | tr.SecondTable td:nth-child(4) {
83 | @media (max-width: 650px) {
84 | ${AdaptiveLastCellBorderRadius}
85 | }
86 | }
87 |
88 | tr:nth-child(even) {
89 | background-color: ${BG_COLOR};
90 | }
91 | td {
92 | color: ${DARK_TEXT_COLOR};
93 | padding: 10px;
94 | overflow: hidden;
95 | white-space: nowrap;
96 | text-overflow: ellipsis;
97 | max-width: 150px;
98 | ${AdaptiveCellVisible}
99 |
100 | &:nth-child(2) {
101 | ${TextBold};
102 | font-weight: bold;
103 | ${GeneralAdaptiveFont}
104 | }
105 | }
106 | `;
107 |
--------------------------------------------------------------------------------
/src/modules/TeamsList/components/Teams/components/TeamsHeader/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 | import {
3 | TeamsHeaderStyled,
4 | TeamsHeaderRightStyled,
5 | TeamsHeaderSubtitleStyled,
6 | TeamsHeaderButtonsBlockStyled,
7 | TeamHeaderLeftStyled,
8 | HeaderManPic,
9 | TeamHeaderTitle,
10 | } from './styled';
11 | import { TeamButton } from 'typography';
12 | import { DARK_TEXT_COLOR, WHITE_COLOR } from 'appConstants/colors';
13 | import { useDispatch } from 'react-redux';
14 | import { useTranslation } from 'react-i18next';
15 | import {
16 | activeModalJoin,
17 | activeModalCreateTeam,
18 | activeModalRemoveCourse,
19 | } from 'modules/TeamsList/teamsListReducer';
20 |
21 | export const TeamsHeader: FC = () => {
22 | const dispatch = useDispatch();
23 | const { t } = useTranslation();
24 | const buttonsInfo: {
25 | name: string;
26 | callback: () => void;
27 | className: string;
28 | }[] = [
29 | {
30 | name: 'Create team',
31 | callback: () => dispatch(activeModalCreateTeam(true)),
32 | className: 'secondStep',
33 | },
34 | {
35 | name: 'Join team',
36 | callback: () => dispatch(activeModalJoin(true)),
37 | className: 'thirdStep',
38 | },
39 | {
40 | name: 'Leave course',
41 | callback: () => dispatch(activeModalRemoveCourse(true)),
42 | className: 'fourthStep',
43 | },
44 | ];
45 |
46 | return (
47 |
48 |
49 | {t('Become a member of the team!')}
50 | {t('To become a member')}
51 |
52 | {buttonsInfo.map((item) => {
53 | return (
54 |
62 | {t(item.name)}
63 |
64 | );
65 | })}
66 |
67 |
68 |
69 |
70 |
71 |
72 | );
73 | };
74 |
--------------------------------------------------------------------------------
/src/modules/TeamsList/components/Teams/components/TeamsHeader/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { WHITE_COLOR, MAIN2_COLOR } from 'appConstants/colors';
3 | import { ReactComponent as HeaderMan } from 'assets/svg/teams-man.svg';
4 | import { PageSubTitle, TextRegular, H2AdaptiveFont, GeneralAdaptiveFont } from 'typography';
5 |
6 | export const TeamsHeaderStyled = styled.div`
7 | padding: 30px;
8 | position: relative;
9 | color: ${WHITE_COLOR};
10 | background-color: ${MAIN2_COLOR};
11 | border-radius: 20px;
12 | z-index: 0;
13 | margin-bottom: 40px;
14 |
15 | @media (max-width: 580px) {
16 | overflow: hidden;
17 | }
18 | `;
19 |
20 | export const TeamHeaderTitle = styled.h2`
21 | ${PageSubTitle};
22 | ${H2AdaptiveFont};
23 | margin: 5px 0 18px;
24 | `;
25 |
26 | export const TeamsHeaderRightStyled = styled.div`
27 | max-width: 631px;
28 | width: 100%;
29 | `;
30 |
31 | export const TeamsHeaderSubtitleStyled = styled.div`
32 | ${TextRegular};
33 | ${GeneralAdaptiveFont};
34 | color: ${WHITE_COLOR};
35 | margin-bottom: 30px;
36 | `;
37 |
38 | export const TeamsHeaderButtonsBlockStyled = styled.div`
39 | display: flex;
40 | gap: 20px;
41 |
42 | @media (max-width: 680px) {
43 | gap: 10px;
44 | }
45 |
46 | @media (max-width: 580px) {
47 | flex-direction: column;
48 | align-content: center;
49 | justify-content: center;
50 | }
51 | `;
52 |
53 | export const TeamHeaderLeftStyled = styled.div`
54 | position: absolute;
55 | right: 0;
56 | top: 0;
57 | bottom: 0;
58 | max-width: 631px;
59 | width: 100%;
60 | z-index: -1;
61 | `;
62 |
63 | export const HeaderManPic = styled(HeaderMan)`
64 | position: absolute;
65 | right: 40px;
66 | bottom: 0;
67 | width: 440px;
68 | height: 254px;
69 | z-index: -1;
70 | `;
71 |
--------------------------------------------------------------------------------
/src/modules/TeamsList/components/Teams/components/index.ts:
--------------------------------------------------------------------------------
1 | export { TeamsHeader } from './TeamsHeader';
2 | export { MyTeam } from './MyTeam';
3 | export { TeamItem } from './TeamItem';
4 |
--------------------------------------------------------------------------------
/src/modules/TeamsList/components/Teams/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 |
3 | import { TeamsHeader, MyTeam, TeamItem } from './components';
4 | import { Team, TeamList } from 'types';
5 | import { TableTitle } from 'modules/StudentsTable/styled';
6 | import { TeamsTitleWrapper } from 'modules/TeamsList/styled';
7 | import { useTranslation } from 'react-i18next';
8 |
9 | type TeamsProps = {
10 | teams: TeamList;
11 | myTeam?: Team;
12 | userId: string;
13 | };
14 |
15 | export const Teams: FC = ({ teams, myTeam, userId }) => {
16 | const { t } = useTranslation();
17 | return (
18 | <>
19 |
20 | {t('Teams')}
21 |
22 | {myTeam ? : }
23 | {!!teams.count &&
24 | teams.results.map((team) => (
25 |
31 | ))}
32 | >
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/src/modules/TeamsList/components/index.ts:
--------------------------------------------------------------------------------
1 | export { Teams } from './Teams';
2 | export { TeamListModals } from './TeamListModals';
3 |
--------------------------------------------------------------------------------
/src/modules/TeamsList/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, useState } from 'react';
2 | import { useSelector, useDispatch } from 'react-redux';
3 | import { useTeamsQuery } from 'hooks/graphql';
4 | import { Loader, ErrorModal, Pagination } from 'components';
5 | import { selectUserData } from 'modules/StudentsTable/selectors';
6 | import { selectCurrCourse } from 'modules/LoginPage/selectors';
7 | import { StyledTeams } from './styled';
8 | import { TEAMS_PER_PAGE, TOUR_OPENING } from 'appConstants';
9 | import { Team } from 'types';
10 | import { TeamListModals, Teams } from './components';
11 | import { useCommonMutations } from './components/TeamListModals/useCommonMutations';
12 | import { AdditionalWrapper, ContentPageWrapper } from 'typography';
13 | import { setIsTourOpen } from 'modules/LoginPage/loginPageReducer';
14 | import { setTourOpening } from 'modules/LoginPage/loginPageMiddleware';
15 |
16 | export const TeamsList: FC = () => {
17 | const [page, setPage] = useState(0);
18 | const currCourse = useSelector(selectCurrCourse);
19 | const userData = useSelector(selectUserData);
20 | const dispatch = useDispatch();
21 | const userTeam = userData.teams?.find((team: Team) => team.courseId === currCourse.id);
22 |
23 | const { loadingT, errorT, teams } = useTeamsQuery({
24 | reactCourseId: currCourse.id,
25 | page,
26 | });
27 |
28 | const loading = loadingT;
29 | const error = errorT;
30 | const {
31 | addUserToTeam,
32 | removeUserFromTeam,
33 | expelUserFromTeam,
34 | createTeam,
35 | updateTeam,
36 | removeUserFromCourse,
37 | commonMutationError,
38 | isLoading,
39 | } = useCommonMutations(page);
40 |
41 | if (!localStorage.getItem(TOUR_OPENING) && !userTeam) {
42 | dispatch(setIsTourOpen(true));
43 | }
44 | dispatch(setTourOpening(TOUR_OPENING));
45 |
46 | if (error || commonMutationError) return ;
47 | if (loading || isLoading) return ;
48 |
49 | const pageCount: number = Math.ceil(teams.count / TEAMS_PER_PAGE);
50 |
51 | return (
52 |
53 |
54 |
55 | {!!teams.results.length && (
56 |
57 | )}
58 |
59 |
60 |
61 |
71 |
72 | );
73 | };
74 |
--------------------------------------------------------------------------------
/src/modules/TeamsList/selectors.ts:
--------------------------------------------------------------------------------
1 | import { State } from 'types';
2 |
3 | export const selectIsActiveModalExpel = (state: State) => state.teamsListReducer.isActiveModalExpel;
4 |
5 | export const selectIsActiveModalLeave = (state: State) => state.teamsListReducer.isActiveModalLeave;
6 |
7 | export const selectIsActiveModalJoin = (state: State) => state.teamsListReducer.isActiveModalJoin;
8 |
9 | export const selectIsActiveModalCreateTeam = (state: State) =>
10 | state.teamsListReducer.isActiveModalCreateTeam;
11 |
12 | export const selectIsActiveModalCreated = (state: State) =>
13 | state.teamsListReducer.isActiveModalCreated;
14 |
15 | export const selectIsActiveModalRemoveCourse = (state: State) =>
16 | state.teamsListReducer.isActiveModalRemoveCourse;
17 |
18 | export const selectIsActiveModalUpdateSocialLink = (state: State) =>
19 | state.teamsListReducer.isActiveModalUpdateSocialLink;
20 |
21 | export const selectIsActiveModalSortStudents = (state: State) =>
22 | state.teamsListReducer.isActiveModalSortStudents;
23 |
24 | export const selectIsActiveModalLeavePage = (state: State) =>
25 | state.teamsListReducer.isActiveModalLeavePage;
26 |
27 | export const selectIsActiveModalCreatedCourse = (state: State) =>
28 | state.teamsListReducer.isActiveModalCreatedCourse;
29 |
30 | export const selectIsActiveModalEditCourse = (state: State) =>
31 | state.teamsListReducer.isActiveModalEditCourse;
32 |
33 | export const selectTeamMemberExpelId = (state: State) => state.teamsListReducer.teamMemberExpelId;
34 |
35 | export const selectTeamPassword = (state: State) => state.teamsListReducer.teamPassword;
36 |
37 | export const selectSocialLink = (state: State) => state.teamsListReducer.socialLink;
38 |
--------------------------------------------------------------------------------
/src/modules/TeamsList/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const StyledTeams = styled.div`
4 | max-width: 1320px;
5 | width: 92%;
6 | `;
7 |
8 | export const TeamsTitleWrapper = styled.div`
9 | position: relative;
10 | display: flex;
11 | align-items: center;
12 | justify-content: space-between;
13 | width: 100%;
14 |
15 | h1 {
16 | width: auto;
17 | }
18 | `;
19 |
--------------------------------------------------------------------------------
/src/modules/TokenPage/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, useEffect } from 'react';
2 | import { AUTH_TOKEN } from 'appConstants';
3 | import { useDispatch } from 'react-redux';
4 | import { useParams, useHistory } from 'react-router-dom';
5 | import { useTranslation } from 'react-i18next';
6 | import { setToken } from 'modules/LoginPage/loginPageReducer';
7 |
8 | type ParamsType = {
9 | id: string;
10 | };
11 |
12 | export const TokenPage: FC = () => {
13 | const dispatch = useDispatch();
14 | const { id } = useParams();
15 | const history = useHistory();
16 | const { t } = useTranslation();
17 |
18 | useEffect(() => {
19 | const loginToken = sessionStorage.getItem(AUTH_TOKEN);
20 | if (!loginToken) {
21 | sessionStorage.setItem(AUTH_TOKEN, id);
22 | dispatch(setToken(id));
23 | }
24 | history.push('/');
25 | }, [id, history, dispatch]);
26 |
27 | return (
28 |
29 |
{t('Redirecting...')}
30 |
31 | );
32 | };
33 |
--------------------------------------------------------------------------------
/src/modules/TutorialPage/components/NoteBlock/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 | import { NoteWrapper, NoteList } from './styled';
3 | import { useTranslation } from 'react-i18next';
4 |
5 | type NoteBlockProps = {
6 | title?: string;
7 | subtitle?: string;
8 | listItems?: string[];
9 | isNote?: string;
10 | };
11 |
12 | export const NoteBlock: FC = ({
13 | title = 'Note',
14 | subtitle,
15 | listItems,
16 | children,
17 | isNote,
18 | }) => {
19 | const { t } = useTranslation();
20 | return (
21 |
22 | {t(title)}
23 | {subtitle && {t(subtitle)}
}
24 | {listItems && (
25 |
26 | {listItems.map((item: string) => (
27 | {t(item)}
28 | ))}
29 |
30 | )}
31 | {children}
32 |
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/src/modules/TutorialPage/components/NoteBlock/styled.ts:
--------------------------------------------------------------------------------
1 | import {
2 | DARK_TEXT_COLOR,
3 | MAIN1_COLOR,
4 | DASHBOARD_HEADER_BG_COLOR,
5 | WHITE_COLOR,
6 | } from 'appConstants/colors';
7 | import styled from 'styled-components';
8 |
9 | type NoteProps = {
10 | isNote?: string;
11 | };
12 |
13 | export const NoteWrapper = styled.div`
14 | display: flex;
15 | flex-direction: column;
16 | width: 100%;
17 | margin-bottom: ${({ isNote }) => isNote && '20px'};
18 | padding: ${({ isNote }) => isNote && '30px'};
19 | background-color: ${({ isNote }) => isNote && DASHBOARD_HEADER_BG_COLOR};
20 | border-radius: 20px;
21 |
22 | p {
23 | margin-top: 0;
24 | }
25 |
26 | & > h4 {
27 | color: ${({ isNote }) => (isNote ? MAIN1_COLOR : DARK_TEXT_COLOR)};
28 | margin: 0 0 20px;
29 | }
30 |
31 | .SelectCourseExample {
32 | width: 300px;
33 | margin-left: 40px;
34 |
35 | @media (max-width: 550px) {
36 | width: 250px;
37 | margin-left: 35px;
38 | }
39 | @media (max-width: 440px) {
40 | width: 200px;
41 | margin-left: 30px;
42 | }
43 | }
44 |
45 | @media (max-width: 650px) {
46 | padding: ${({ isNote }) => isNote && '27.5px'};
47 | }
48 | @media (max-width: 550px) {
49 | padding: ${({ isNote }) => isNote && '25px'};
50 | }
51 | @media (max-width: 440px) {
52 | padding: ${({ isNote }) => isNote && '20px'};
53 | margin-bottom: ${({ isNote }) => isNote && '10px'};
54 | }
55 | `;
56 |
57 | export const NoteList = styled.ol`
58 | display: flex;
59 | flex-direction: column;
60 | gap: 25px;
61 | list-style: none;
62 | counter-reset: counter;
63 | margin: 0 0 20px;
64 | padding-inline-start: 0;
65 |
66 | li {
67 | counter-increment: counter;
68 | margin: 0 0 0 40px;
69 | text-align: justify;
70 | @media (max-width: 550px) {
71 | margin-left: 35px;
72 | }
73 | @media (max-width: 440px) {
74 | margin-left: 30px;
75 | }
76 | }
77 |
78 | li::before {
79 | content: counter(counter);
80 | background-color: ${MAIN1_COLOR};
81 | width: 30px;
82 | height: 30px;
83 | border-radius: 50%;
84 | display: inline-block;
85 | line-height: 30px;
86 | color: ${WHITE_COLOR};
87 | text-align: center;
88 | margin: 0 10px -3px -40px;
89 |
90 | @media (max-width: 550px) {
91 | width: 25px;
92 | height: 25px;
93 | line-height: 25px;
94 | margin-left: -35px;
95 | }
96 | @media (max-width: 440px) {
97 | width: 20px;
98 | height: 20px;
99 | line-height: 22px;
100 | margin-left: -30px;
101 | }
102 | }
103 |
104 | @media (max-width: 550px) {
105 | gap: 20px;
106 | }
107 | @media (max-width: 440px) {
108 | gap: 15px;
109 | }
110 | `;
111 |
--------------------------------------------------------------------------------
/src/modules/TutorialPage/components/StepBlock/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 | import { useTranslation } from 'react-i18next';
3 | import { StepBlockWrapper, StepBlockText } from './styled';
4 | import { PageTitle } from 'typography';
5 |
6 | type StepBlockProps = {
7 | title: string;
8 | subtitle: string;
9 | imageSrc: string;
10 | altText: string;
11 | };
12 |
13 | export const StepBlock: FC = ({ title, subtitle, imageSrc, altText }) => {
14 | const { t } = useTranslation();
15 | return (
16 |
17 | {t(title)}
18 | {t(subtitle)}
19 |
20 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/src/modules/TutorialPage/components/StepBlock/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { TextRegular } from 'typography';
3 |
4 | export const StepBlockWrapper = styled.div`
5 | display: flex;
6 | flex-direction: column;
7 | width: 100%;
8 | height: 100%;
9 |
10 | & > * {
11 | margin-bottom: 20px;
12 | }
13 | `;
14 |
15 | export const StepBlockText = styled.p`
16 | ${TextRegular};
17 | margin-top: 0;
18 | `;
19 |
--------------------------------------------------------------------------------
/src/modules/TutorialPage/components/index.ts:
--------------------------------------------------------------------------------
1 | export { StepBlock } from './StepBlock';
2 | export { NoteBlock } from './NoteBlock';
3 |
--------------------------------------------------------------------------------
/src/modules/TutorialPage/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, useCallback } from 'react';
2 | import { TutorialPageWrapper } from './styled';
3 | import editProfileExampleEN from 'assets/images/editProfileExampleEN.png';
4 | import editProfileExampleRU from 'assets/images/editProfileExampleRU.png';
5 | import teamActionsExampleEN from 'assets/images/teamActionsExampleEN.png';
6 | import teamActionsExampleRU from 'assets/images/teamActionsExampleRU.png';
7 | import { NoteBlock, StepBlock } from './components';
8 | import { tutorialNoteInfo } from './tutorialPageInfo';
9 | import { useSelector } from 'react-redux';
10 | import { selectCurrLanguage } from 'modules/LoginPage/selectors';
11 | import { ContentPageWrapper } from 'typography';
12 | import { useTranslation } from 'react-i18next';
13 |
14 | export const TutorialPage: FC = () => {
15 | const currentLang = useSelector(selectCurrLanguage);
16 | const { t } = useTranslation();
17 | const TUTORIAL_PAGE_NOTES_INFO = useCallback(
18 | () => tutorialNoteInfo(currentLang, t),
19 | [currentLang, t]
20 | );
21 |
22 | return (
23 |
24 |
25 |
31 |
35 |
41 | {TUTORIAL_PAGE_NOTES_INFO().map(({ title, subtitle, listItems, isNote, children }) => (
42 |
46 | {children}
47 |
48 | ))}
49 |
50 |
51 | );
52 | };
53 |
--------------------------------------------------------------------------------
/src/modules/TutorialPage/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { GeneralAdaptiveFont } from 'typography';
3 |
4 | export const TutorialPageWrapper = styled.div`
5 | display: flex;
6 | flex-direction: column;
7 | width: 680px;
8 | height: fit-content;
9 | padding: 60px 0;
10 | gap: 40px;
11 |
12 | .StepBlockImage {
13 | margin: 20px 0;
14 | border-radius: 20px;
15 | box-shadow: 0px 4px 50px rgba(6, 73, 140, 0.1);
16 |
17 | @media (max-width: 440px) {
18 | margin: 0;
19 | }
20 | }
21 |
22 | div:nth-of-type(6) {
23 | margin-top: -17px;
24 | }
25 |
26 | div:last-child {
27 | margin-top: 20px;
28 | & > p {
29 | margin-bottom: 0;
30 | }
31 | }
32 |
33 | p,
34 | li {
35 | ${GeneralAdaptiveFont};
36 | }
37 |
38 | h4 {
39 | font-size: 20px;
40 | line-height: 30px;
41 | }
42 |
43 | @media (max-width: 992px) {
44 | width: 640px;
45 |
46 | h4 {
47 | font-size: 1.3rem;
48 | }
49 | }
50 | @media (max-width: 768px) {
51 | width: 600px;
52 |
53 | h1 {
54 | font-size: 1.3rem;
55 | }
56 | h4 {
57 | font-size: 1.2rem;
58 | }
59 | }
60 | @media (max-width: 650px) {
61 | width: 500px;
62 | padding: 55px 0;
63 | gap: 30px;
64 |
65 | div:last-child {
66 | margin-top: 0;
67 | }
68 |
69 | h1 {
70 | font-size: 1.25rem;
71 | }
72 | h4 {
73 | font-size: 1.15rem;
74 | }
75 | }
76 | @media (max-width: 550px) {
77 | width: 400px;
78 | padding: 50px 0;
79 | gap: 25px;
80 |
81 | h1 {
82 | font-size: 1.1rem;
83 | }
84 | h4 {
85 | font-size: 1rem;
86 | }
87 | }
88 | @media (max-width: 440px) {
89 | width: 280px;
90 | padding: 40px 0;
91 | gap: 20px;
92 |
93 | h1 {
94 | font-size: 1rem;
95 | }
96 | h4 {
97 | font-size: 0.9rem;
98 | line-height: 22px;
99 | }
100 | }
101 | `;
102 |
--------------------------------------------------------------------------------
/src/modules/TutorialPage/tutorialPageInfo.tsx:
--------------------------------------------------------------------------------
1 | import myTeamExampleEN from 'assets/images/myTeamExampleEN.png';
2 | import myTeamExampleRU from 'assets/images/myTeamExampleRU.png';
3 |
4 | export const tutorialNoteInfo = (currentLang: string, t: any) => {
5 | const TUTORIAL_PAGE_NOTES_INFO = [
6 | {
7 | title: 'Create team',
8 | subtitle: 'The team creation process is taken place in three steps',
9 | listItems: [
10 | 'Press the “Create team” button',
11 | 'Provide a link to a team chat',
12 | 'Specify password',
13 | ],
14 | children: {t('The only person who creates the team is Team Lead')}
,
15 | },
16 | {
17 | title: 'Join team',
18 | subtitle: 'To join an existing team',
19 | listItems: [`Press the “Join team” button`, `Specify the password`],
20 | },
21 | {
22 | listItems: [
23 | `If you have not found a team`,
24 | `If the team has fewer members than is required for the task`,
25 | ],
26 | isNote: 'Note',
27 | },
28 | {
29 | title: 'You will see the following after joining the team',
30 | children: (
31 |
36 | ),
37 | },
38 | {
39 | title: 'Leave team',
40 | subtitle: 'To leave the team',
41 | },
42 | ];
43 | return TUTORIAL_PAGE_NOTES_INFO;
44 | };
45 |
--------------------------------------------------------------------------------
/src/modules/index.ts:
--------------------------------------------------------------------------------
1 | export { LoginPage } from './LoginPage';
2 | export { StudentsTable } from './StudentsTable';
3 | export { TeamsList } from './TeamsList';
4 | export { TokenPage } from './TokenPage';
5 | export { NotFoundPage } from './NotFoundPage';
6 | export { EditProfile } from './EditProfile';
7 | export { TutorialPage } from './TutorialPage';
8 | export { AdminPage } from './AdminPage';
9 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | declare module '*.woff2';
3 | declare module '*.svg' {
4 | import React = require('react');
5 | export const ReactComponent: React.SFC>;
6 | const src: string;
7 | export default src;
8 | }
9 |
10 | declare module '*.jpg' {
11 | const content: string;
12 | export default content;
13 | }
14 |
15 | declare module '*.png' {
16 | const content: string;
17 | export default content;
18 | }
19 |
--------------------------------------------------------------------------------
/src/reportWebVitals.ts:
--------------------------------------------------------------------------------
1 | import { ReportHandler } from 'web-vitals';
2 |
3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => {
4 | if (onPerfEntry && onPerfEntry instanceof Function) {
5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
6 | getCLS(onPerfEntry);
7 | getFID(onPerfEntry);
8 | getFCP(onPerfEntry);
9 | getLCP(onPerfEntry);
10 | getTTFB(onPerfEntry);
11 | });
12 | }
13 | };
14 |
15 | export default reportWebVitals;
16 |
--------------------------------------------------------------------------------
/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/src/store/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 | import { Provider } from 'react-redux';
3 | import { studentsTableReducer } from 'modules/StudentsTable/studentsTableReducer';
4 | import { teamsListReducer } from 'modules/TeamsList/teamsListReducer';
5 | import { loginPageReducer } from 'modules/LoginPage/loginPageReducer';
6 | import { createStore, combineReducers, applyMiddleware } from 'redux';
7 | import { composeWithDevTools } from 'redux-devtools-extension/developmentOnly';
8 | import thunkMiddleware from 'redux-thunk';
9 | import { State } from 'types';
10 |
11 | const appReducer = combineReducers({
12 | studentsTableReducer,
13 | teamsListReducer,
14 | loginPageReducer,
15 | });
16 |
17 | const store = createStore(appReducer, composeWithDevTools(applyMiddleware(thunkMiddleware)));
18 |
19 | export const AppState: FC = ({ children }) => {
20 | return (
21 |
22 | <>{children}>
23 |
24 | );
25 | };
26 |
--------------------------------------------------------------------------------
/src/translation/resources.ts:
--------------------------------------------------------------------------------
1 | import { CURRENT_LANG, DEFAULT_LANGUAGE } from 'appConstants';
2 | import i18n from 'i18next';
3 | import { initReactI18next } from 'react-i18next';
4 |
5 | import translationEN from './en/en.json';
6 | import translationRU from './ru/ru.json';
7 |
8 | const resources = {
9 | en: {
10 | translation: translationEN,
11 | },
12 | ru: {
13 | translation: translationRU,
14 | },
15 | };
16 |
17 | const language = localStorage.getItem(CURRENT_LANG) ?? DEFAULT_LANGUAGE;
18 |
19 | i18n.use(initReactI18next).init({
20 | resources,
21 | lng: language,
22 | keySeparator: false,
23 | interpolation: {
24 | escapeValue: false,
25 | },
26 | });
27 |
28 | export default i18n;
29 |
--------------------------------------------------------------------------------
/src/typography/common.css:
--------------------------------------------------------------------------------
1 | *,
2 | *::before,
3 | *::after {
4 | box-sizing: border-box;
5 | }
6 |
7 | :root {
8 | --BG_COLOR: #f2f8fd;
9 | --OVERLAY_COLOR: rgba(54, 61, 72, 0.3);
10 | --SCROLL_THUMB_COLOR: #1e33570d;
11 | --WHITE_COLOR: #ffffff;
12 | }
13 |
14 | body {
15 | margin: 0;
16 | font: normal 16px/24px "Poppins", sans-serif;
17 | background-color: var(--BG_COLOR);
18 |
19 | -webkit-font-smoothing: antialiased;
20 | -moz-osx-font-smoothing: grayscale;
21 | }
22 |
23 | input {
24 | background-color: var(--BG_COLOR);
25 | }
26 |
--------------------------------------------------------------------------------
/src/typography/fonts.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-style: normal;
3 | font-weight: 400;
4 | font-family: "Poppins";
5 | src: url("../assets/fonts/Poppins-Regular-400.otf") format("otf"),
6 | url("../assets/fonts/Poppins-Regular-400.woff2") format("woff2"),
7 | url("../assets/fonts/Poppins-Regular-400.woff") format("woff"),
8 | url("../assets/fonts/Poppins-Regular-400.ttf") format("truetype");
9 | }
10 |
11 | @font-face {
12 | font-style: normal;
13 | font-weight: 500;
14 | font-family: "Poppins";
15 | src: url("../assets/fonts/Poppins-Medium-500.otf") format("otf"),
16 | url("../assets/fonts/Poppins-Medium-500.woff") format("woff"),
17 | url("../assets/fonts/Poppins-Medium-500.woff2") format("woff2")
18 | url("../assets/fonts/Poppins-Medium-500.ttf") format("truetype");
19 | }
20 |
21 | @font-face {
22 | font-style: normal;
23 | font-weight: 600;
24 | font-family: "Poppins";
25 | src: url("../assets/fonts/Poppins-SemiBold-600.otf") format("otf"),
26 | url("../assets/fonts/Poppins-SemiBold-600.woff") format("woff"),
27 | url("../assets/fonts/Poppins-SemiBold-600.woff2") format("woff2"),
28 | url("../assets/fonts/Poppins-SemiBold-600.ttf") format("truetype");
29 | }
30 |
31 | @font-face {
32 | font-style: normal;
33 | font-weight: 700;
34 | font-family: "Poppins";
35 | src: url("../assets/fonts/Poppins-Bold-700.otf") format("otf"),
36 | url("../assets/fonts/Poppins-Bold-700.woff") format("woff"),
37 | url("../assets/fonts/Poppins-Bold-700.woff2") format("woff2"),
38 | url("../assets/fonts/Poppins-Bold-700.ttf") format("truetype");
39 | }
40 |
--------------------------------------------------------------------------------
/src/utils/isFieldValid.ts:
--------------------------------------------------------------------------------
1 | export const isFieldValid = (
2 | value: string,
3 | validateRules: any,
4 | needValidate: boolean,
5 | setInputValid: (valid: boolean) => void,
6 | setErrorMessage: (message: string) => void,
7 | isValueUniq?: (courseName: string) => boolean
8 | ) => {
9 | if (!needValidate) return true;
10 |
11 | const trimmedValueLength = value.trim().length;
12 | let valid = true;
13 |
14 | if (validateRules.maxLength) {
15 | valid = trimmedValueLength < validateRules.maxLength.value + 1 && valid;
16 | if (!(trimmedValueLength < validateRules.maxLength.value + 1)) {
17 | setErrorMessage(validateRules.maxLength.message);
18 | }
19 | }
20 |
21 | if (validateRules.minLength) {
22 | valid = trimmedValueLength >= validateRules.minLength.value && valid;
23 | if (!(trimmedValueLength >= validateRules.minLength.value)) {
24 | setErrorMessage(validateRules.minLength.message);
25 | }
26 | }
27 |
28 | if (validateRules.pattern) {
29 | const regExp = new RegExp(validateRules.pattern.value);
30 | valid = regExp.test(value) && valid;
31 | if (!regExp.test(value)) {
32 | setErrorMessage(validateRules.pattern.message);
33 | }
34 | }
35 |
36 | if (validateRules.uniq && isValueUniq) {
37 | const isFieldValueUniq = isValueUniq(value.trim());
38 | valid = isFieldValueUniq && valid;
39 | if (!isFieldValueUniq) {
40 | setErrorMessage(validateRules.uniq.message);
41 | }
42 | }
43 |
44 | setInputValid(valid);
45 | };
46 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react-jsx",
22 | "baseUrl": "./src"
23 | },
24 | "include": [
25 | "src"
26 | ]
27 | }
28 |
--------------------------------------------------------------------------------