Repository: rolling-scopes-school/RSS-Teams-FE
Branch: master
Commit: 87986aad70b2
Files: 199
Total size: 268.1 KB
Directory structure:
gitextract_043m4_he/
├── .eslintrc.js
├── .firebaserc
├── .github/
│ └── workflows/
│ └── deploy-client.yml
├── .gitignore
├── .prettierrc
├── README.md
├── firebase.json
├── package.json
├── pre-build.js
├── public/
│ ├── index.html
│ ├── manifest.json
│ └── robots.txt
├── src/
│ ├── appConstants/
│ │ ├── api.ts
│ │ ├── colors.ts
│ │ └── index.ts
│ ├── assets/
│ │ └── fonts/
│ │ ├── Poppins-Bold-700.otf
│ │ ├── Poppins-Medium-500.otf
│ │ ├── Poppins-Regular-400.otf
│ │ └── Poppins-SemiBold-600.otf
│ ├── 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
================================================
FILE CONTENTS
================================================
================================================
FILE: .eslintrc.js
================================================
module.exports = {
parser: '@typescript-eslint/parser',
extends: [
'plugin:react/recommended',
'plugin:@typescript-eslint/recommended',
'prettier/@typescript-eslint',
'plugin:react-hooks/recommended',
'plugin:prettier/recommended',
],
plugins: ['@typescript-eslint', 'react', 'prettier', 'react-hooks'],
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
rules: {
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
'comma-dangle': ['error', 'only-multiline'],
'react/prop-types': 'off',
'react/display-name': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'prettier/prettier': ['error', { endOfLine: 'auto' }],
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/ban-ts-ignore': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-var-reqiures': 'off',
},
settings: {
react: {
version: 'detect',
},
},
globals: { React: 'writable' },
};
================================================
FILE: .firebaserc
================================================
{
"projects": {
"default": "rss-teams"
}
}
================================================
FILE: .github/workflows/deploy-client.yml
================================================
name: Build and Deploy
on:
push:
branches:
- master
jobs:
admin:
name: Deploy PROD
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@master
- name: Install Dependencies
run: npm install
- name: Build
run: npm run build
- name: Archive Production Artifact
uses: actions/upload-artifact@master
with:
name: build
path: build
- name: Deploy to Firebase
uses: w9jds/firebase-action@master
with:
args: deploy --only hosting
env:
FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}
================================================
FILE: .gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
/.eslintcache*
# testing
/coverage
# production
/build
/.firebase
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.eslintcache*
================================================
FILE: .prettierrc
================================================
{
"endOfLine": "auto",
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 100
}
================================================
FILE: README.md
================================================
# RSS-Teams-FE - Deprecated and no longer maintained. The latest deployed version will become unavailable from 28/10/2022 due to Heroku limitations.
Link to deployment - https://rss-teams.web.app/login
Stack:
1 React - https://reactjs.org/
2 Redux, redux-thunk, redux-devtools-extension - https://redux.js.org/
3 Typescript - https://www.typescriptlang.org/
4 Apollo - https://www.howtographql.com/
Used libs:
1 styled-components - https://askd.rocks/pres/styled-gdg/
2 react-router/react-router-dom - https://reactrouter.com/
3 react-hook-form
4 react-window
5 react-paginate
6 reactour
Linters: prettier, eslint.
Branch RSSFE-01 contains login via github made frontend way.
App description:
The application is intended for use by students of The Rolling Scopes School. The purpose of the application is to enable students to collect teams from friends, and single students - to be automatically assigned to teams.
Backend repo: https://github.com/rolling-scopes-school/RSS-Teams-BE
MindMap: https://miro.com/welcomeonboard/2TsmiaGCbWQVZGhwyEW6EDfjdeQpOnKt6Hl62GvbVkT3ky3h02vqWHvI2gCz76cG
# Getting Started with Create React App
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `yarn start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.\
You will also see any lint errors in the console.
### `yarn test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `yarn build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `yarn eject`
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
================================================
FILE: firebase.json
================================================
{
"hosting": {
"public": "build",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [
{
"source": "**",
"destination": "/index.html"
}
]
}
}
================================================
FILE: package.json
================================================
{
"name": "rss-teams-fe",
"version": "0.1.0",
"private": true,
"dependencies": {
"@apollo/client": "^3.3.7",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"@types/jest": "^26.0.15",
"@types/node": "^12.0.0",
"@types/react": "^16.9.53",
"@types/react-dom": "^16.9.8",
"@types/react-router-dom": "^5.1.7",
"@types/react-virtualized-auto-sizer": "^1.0.0",
"@types/react-window": "^1.8.2",
"@types/reactour": "^1.18.1",
"@types/redux-actions": "^2.6.1",
"@types/styled-components": "^5.1.7",
"graphql": "^15.5.0",
"i18next": "^19.9.1",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-hook-form": "^6.15.1",
"react-i18next": "^11.8.8",
"react-paginate": "^7.0.0",
"react-redux": "^7.2.2",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.3",
"react-virtualized-auto-sizer": "^1.0.4",
"react-window": "^1.8.6",
"reactour": "^1.18.3",
"redux": "^4.0.5",
"redux-actions": "^2.6.5",
"redux-thunk": "^2.3.0",
"styled-components": "^5.2.1",
"typescript": "4.2.4",
"web-vitals": "^0.2.4"
},
"scripts": {
"start": "react-scripts start",
"build": "npm run pre-build && react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"pre-build": "node pre-build",
"deploy": "firebase deploy",
"lint:eslint": "eslint \"{,!(node_modules)/**/}*.{ts,tsx}\"",
"fix:prettier": "prettier --write \"{,!(node_modules)/**/}*.{ts,tsx}\"",
"fix:eslint": "eslint --fix \"{,!(node_modules)/**/}*.{{ts,tsx}\""
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@types/react-paginate": "^6.2.1",
"@types/react-redux": "^7.1.16",
"@types/redux": "^3.6.0",
"@typescript-eslint/eslint-plugin": "^4.14.1",
"@typescript-eslint/parser": "^4.14.1",
"eslint": "^7.19.0",
"eslint-config-prettier": "^7.2.0",
"eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-react": "^7.22.0",
"eslint-plugin-react-hooks": "^4.2.0",
"prettier": "^2.2.1",
"redux-devtools-extension": "^2.13.8"
}
}
================================================
FILE: pre-build.js
================================================
/* eslint-disable @typescript-eslint/no-var-requires */
const fs = require('fs');
require('dotenv').config();
const BACKEND_LINK = `export const BACKEND_LINK = 'https://rss-teams.herokuapp.com/graphql';
`;
const AUTH_BACKEND_LINK = `export const AUTH_BACKEND_LINK = 'https://rss-teams.herokuapp.com/auth/github/';
`;
fs.writeFileSync('./src/appConstants/api.ts', BACKEND_LINK + AUTH_BACKEND_LINK);
================================================
FILE: public/index.html
================================================
RSS Teams
================================================
FILE: public/manifest.json
================================================
{
"short_name": "RSS Teams",
"name": "The Rolling Scopes School Teams",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "favicon.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "favicon.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
================================================
FILE: public/robots.txt
================================================
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
================================================
FILE: src/appConstants/api.ts
================================================
export const BACKEND_LINK = 'https://rss-teams-dev.herokuapp.com/graphql';
export const AUTH_BACKEND_LINK = 'https://rss-teams-dev.herokuapp.com/auth/github/';
================================================
FILE: src/appConstants/colors.ts
================================================
export const WHITE_COLOR = '#FFFFFF';
export const BG_COLOR = '#F2F8FD';
export const MAIN1_COLOR = '#6550F6'; // violet
export const MAIN1_DARK_COLOR = '#5039EF'; // dark violet
export const MAIN2_COLOR = '#FA6678'; // red
export const MAIN2_LIGHT_COLOR = '#FE7888'; // light red
export const LIGHT_TEXT_COLOR = '#7E96C2';
export const DARK_TEXT_COLOR = '#363D48';
export const OVERLAY_COLOR = '#363D4866';
export const DASHBOARD_HEADER_BG_COLOR = '#E1EEFA';
export const TABLE_SCROLLBAR_BG_COLOR = '#F9F9FD';
export const TABLE_SCROLLBAR_THUMB_COLOR = '#1E33570D';
export const TABLE_POPUP_BORDER_COLOR = '#363D4833';
export const FOOTER_NAMES_COLOR = '#9CA5B5';
export const ALERT_COLOR = '#FA6678'; // red
================================================
FILE: src/appConstants/index.ts
================================================
export { BACKEND_LINK, AUTH_BACKEND_LINK } from './api';
export const SET_USER_DATA = 'SET_USER_DATA';
export const AUTH_TOKEN = 'AUTH_TOKEN';
export const SET_TOKEN = 'SET_TOKEN';
export const SET_CURR_COURSE = 'SET_CURR_COURSE';
export const SET_COMMON_ERROR = 'SET_COMMON_ERROR';
export const SET_BURGER_MENU_OPEN = 'SET_BURGER_MENU_OPEN';
export const ACTIVE_MODAL_EXPEL = 'ACTIVE_MODAL_EXPEL';
export const ACTIVE_MODAL_LEAVE = 'ACTIVE_MODAL_LEAVE';
export const ACTIVE_MODAL_JOIN = 'ACTIVE_MODAL_JOIN';
export const ACTIVE_MODAL_CREATE_TEAM = 'ACTIVE_MODAL_CREATE_TEAM';
export const ACTIVE_MODAL_CREATED = 'ACTIVE_MODAL_CREATED';
export const ACTIVE_MODAL_UPDATE_SOCIAL_LINK = 'ACTIVE_MODAL_UPDATE_SOCIAL_LINK';
export const ACTIVE_MODAL_REMOVE_COURSE = 'ACTIVE_MODAL_REMOVE_COURSE';
export const ACTIVE_MODAL_SORT_STUDENTS = 'ACTIVE_MODAL_SORT_STUDENTS';
export const ACTIVE_MODAL_LEAVE_PAGE = 'ACTIVE_MODAL_LEAVE_PAGE';
export const ACTIVE_MODAL_CREATED_COURSE = 'ACTIVE_MODAL_CREATED_COURSE';
export const ACTIVE_MODAL_EDIT_COURSE = 'ACTIVE_MODAL_EDIT_COURSE';
export const SET_TEAM_MEMBER_EXPEL_ID = 'SET_TEAM_MEMBER_EXPEL_ID';
export const SET_TEAM_PASSWORD = 'SET_TEAM_PASSWORD';
export const SET_SOCIAL_LINK = 'SET_SOCIAL_LINK';
export const SET_FILTER_DATA = 'SET_FILTER_DATA';
export const SET_CURR_LANG = 'SET_CURR_LANG';
export const SET_EDIT_PROFILE_DATA_CHANGE = 'SET_EDIT_PROFILE_DATA_CHANGE';
export const SET_PATH_TO_THE_PAGE = 'SET_PATH_TO_THE_PAGE';
export const SET_IS_TOUR_OPEN = 'SET_IS_TOUR_OPEN';
export const USERS_PER_PAGE = 20;
export const TEAMS_PER_PAGE = 10;
export const CURRENT_YEAR = new Date(Date.now()).getFullYear();
export const CURRENT_COURSE = 'currentCourse';
export const CURRENT_LANG = 'currentLanguage';
export const TOUR_OPENING = 'tourOpening';
export const TABLE_HEADERS = [
'№',
'First / Last Name',
'Score',
'Team Number',
'Telegram',
'Discord',
'Github',
'Location',
'Courses',
];
export const TABLE_TEAMS_HEADERS = [
'№',
'First / Last Name',
'Score',
'Telegram',
'Discord',
'Github',
'Location',
'Action',
];
export const INPUT_VALUES_EDIT_PROFILE: string[] = [
'firstName',
'lastName',
'discord',
'telegram',
'city',
'country',
'score',
];
export const MODAL_INPUT_VALIDATION = {
pattern: {
value:
/^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?|^((http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/,
message: 'Use the format',
},
maxLength: {
value: 55,
message: 'This input exceed maxLength.',
},
};
export const COURSE_NAME_VALIDATION = {
pattern: {
value: `${CURRENT_YEAR}`,
message: 'Please, enter correct course name',
},
minLength: {
value: 5,
message: 'Minimal length is 5.',
},
maxLength: {
value: 55,
message: 'This input exceed maxLength.',
},
uniq: {
message: 'Please, enter unique course name',
},
};
export const TEAM_SIZE_VALIDATION = {
pattern: {
value: /^[2-9]$/i,
message: 'Please, enter correct team size',
},
minLength: {
value: 1,
message: 'Minimal length is 1.',
},
maxLength: {
value: 1,
message: 'This input exceed maxLength.',
},
};
export const APP_NAVIGATION_LINKS = {
['/students']: {
name: 'Dashboard',
isAlwaysVisible: false,
},
['/']: {
name: 'Teams',
isAlwaysVisible: false,
},
['/edit-profile']: {
name: 'Edit Profile',
isAlwaysVisible: true,
},
['/tutorial']: {
name: 'Tutorial',
isAlwaysVisible: true,
},
['/admin']: {
name: 'Admin',
isAlwaysVisible: true,
},
};
export const DEFAULT_LANGUAGE = 'en';
export const LANGUAGES: string[] = [DEFAULT_LANGUAGE, 'ru'];
export const Language: { [key: string]: string } = {
en: 'EN',
ru: 'RU',
};
export const FOOTER_INFO = [
{
title: 'Development',
members: ['besovadevka', 'MadaShindeInai', 'self067', 'manuminsk', 'Malagor', 'dariavv'],
},
{
title: 'Design',
members: ['Nastya Kapylova'],
},
];
export const LINK_TO_DESIGN_BLOCK = 'https://www.linkedin.com/in/nastya-kapylova-54126215a';
export const LINK_TO_REPO = 'https://github.com/rolling-scopes-school/RSS-Teams-FE';
export const addCourseInputsInfo = [
{ label: 'Course name', placeholder: 'Enter course name' },
{ label: 'Team size', placeholder: 'Enter team size' },
];
export const SHOW_COURSES_OPTIONS = [
{ id: '1', name: 'All' },
{ id: '2', name: 'Active' },
{ id: '3', name: 'Terminated' },
];
export const UNAUTHORIZED_ERROR_MESSAGE = 'Unauthorized';
================================================
FILE: src/components/App/index.tsx
================================================
import React, { FC, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Route, Switch } from 'react-router-dom';
import {
TeamsList,
LoginPage,
StudentsTable,
TokenPage,
NotFoundPage,
EditProfile,
TutorialPage,
AdminPage,
} from 'modules';
import { Loader, PrivateRoute, Header, Footer, ErrorModal, TourGuide } from 'components';
import { selectToken } from 'modules/LoginPage/selectors';
import { AUTH_TOKEN, CURRENT_COURSE, CURRENT_LANG, DEFAULT_LANGUAGE } from 'appConstants';
import { useWhoAmIQuery } from 'hooks/graphql';
import { AppStyled } from './styled';
import { setToken } from 'modules/LoginPage/loginPageReducer';
import { setUserData } from 'modules/StudentsTable/studentsTableReducer';
import { setCourse, setLanguage } from 'modules/LoginPage/loginPageMiddleware';
export const App: FC = () => {
const dispatch = useDispatch();
const loginToken = useSelector(selectToken);
const [loading, setLoading] = useState(true);
const { loadingW, whoAmI, errorW } = useWhoAmIQuery({
skip: loginToken === null,
});
const newUserCheck = !!whoAmI?.courses.length;
const isUserAdmin = !!whoAmI?.isAdmin;
useEffect(() => {
if (!loginToken) {
const token = sessionStorage.getItem(AUTH_TOKEN);
if (token) dispatch(setToken(token));
}
if (!!whoAmI) {
dispatch(setUserData(whoAmI));
}
if (whoAmI?.courses[0]) {
dispatch(setLanguage(localStorage.getItem(CURRENT_LANG) ?? DEFAULT_LANGUAGE));
dispatch(
setCourse(JSON.parse(localStorage.getItem(CURRENT_COURSE) as string) ?? whoAmI?.courses[0])
);
}
if (!loadingW) setLoading(false);
}, [dispatch, loginToken, loadingW, loading, whoAmI]);
if (errorW) return ;
if (loading || loadingW) return ;
return (
{!!loginToken && }
);
};
================================================
FILE: src/components/App/styled.ts
================================================
import styled from 'styled-components';
export const AppStyled = styled.div`
position: relative;
height: 100vh;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
`;
================================================
FILE: src/components/CommonSelectList/index.tsx
================================================
import React, { FC, useEffect, useRef } from 'react';
import { Course } from 'types';
import {
StyledCoursesSelectWrapper,
StyledCoursesSelectHeaderWrapper,
StyledCoursesList,
StyledCoursesSelectInfo,
StyledCoursesSelectArrow,
} from './styled';
import { useTranslation } from 'react-i18next';
import { Language } from 'appConstants';
type CommonSelectProps = {
displayList: boolean;
setDisplayList: (display: boolean) => void;
listItems: any;
onClickHandler: any;
title?: string;
currItem: string;
isLang?: boolean;
menuToggle?: boolean;
customStyle?: boolean;
showOptionsSelect?: boolean;
};
export const CommonSelectList: FC = ({
displayList,
setDisplayList,
listItems,
onClickHandler,
title,
currItem,
isLang,
menuToggle,
customStyle,
showOptionsSelect,
}) => {
const selectRef = useRef(null);
const { t } = useTranslation();
useEffect(() => {
const closeEventHandler = (ev: MouseEvent) => {
if ((selectRef.current as unknown as HTMLDivElement)?.contains(ev.target as HTMLDivElement)) {
setDisplayList(!displayList);
return;
}
setDisplayList(false);
};
document && document.addEventListener('click', closeEventHandler);
return (): void => {
document && document.removeEventListener('click', closeEventHandler);
};
}, [displayList, setDisplayList]);
const isListItemsExists = !!listItems.length;
return (
{title && {t(title)}
}
{t(currItem)}
{isListItemsExists && }
{' '}
{isListItemsExists && (
{listItems.map((item: Course | string) => {
return (
onClickHandler(item)}
>
{typeof item === 'string' ? Language[item] : t(item.name)}
);
})}
)}
);
};
================================================
FILE: src/components/CommonSelectList/styled.ts
================================================
import {
MAIN1_DARK_COLOR,
WHITE_COLOR,
MAIN1_COLOR,
DARK_TEXT_COLOR,
BG_COLOR,
LIGHT_TEXT_COLOR,
} from 'appConstants/colors';
import styled from 'styled-components';
import { ReactComponent as CoursesSelectArrow } from 'assets/svg/coursesSelectArrow.svg';
import { HeaderAdaptiveFont, SVGArrowAdaptive } from 'typography';
type TStyledCoursesSelectInfo = {
hover: boolean;
} & TFooterProp;
type TStyledCoursesSelectList = {
isClicked: boolean;
showOptionsSelect?: boolean;
} & TFooterProp;
type TFooterProp = {
isLang?: boolean;
menuToggle?: boolean;
customStyle?: boolean;
};
export const StyledCoursesSelectWrapper = styled.div`
position: ${({ showOptionsSelect }) => showOptionsSelect && 'absolute'};
left: ${({ showOptionsSelect }) => showOptionsSelect && '5%'};
z-index: 1;
display: ${({ menuToggle }) => (menuToggle ? 'none' : 'flex')};
flex-direction: column;
width: ${({ isLang }) => (isLang ? '82px' : '300px')};
height: fit-content;
min-height: 40px;
margin: ${({ isLang }) => !isLang && '0 20px 0 0'};
overflow: hidden;
font: 400 1rem/24px 'Poppins', sans-serif;
color: ${({ customStyle }) => (customStyle ? DARK_TEXT_COLOR : WHITE_COLOR)};
background-color: ${({ customStyle }) => (customStyle ? BG_COLOR : MAIN1_DARK_COLOR)};
border-radius: 10px;
${HeaderAdaptiveFont}
ul {
margin-top: ${({ isClicked }) => (isClicked ? '-5px' : '-150%')};
}
@media (max-width: 1100px) {
left: ${({ showOptionsSelect }) => showOptionsSelect && '9%'};
top: ${({ showOptionsSelect }) => showOptionsSelect && '50%'};
}
@media (max-width: 768px) {
left: ${({ showOptionsSelect }) => showOptionsSelect && '10%'};
}
@media (max-width: 700px) {
width: ${({ isLang }) => !isLang && '260px'};
}
@media (max-width: 600px) {
display: ${({ isLang }) => isLang && 'none'};
display: ${({ menuToggle }) => menuToggle && 'flex'};
margin: ${({ isLang }) => !isLang && '0 20px 0 0'};
}
@media (max-width: 440px) {
width: ${({ isLang }) => !isLang && '200px'};
}
@media (max-width: 375px) {
width: ${({ isLang }) => !isLang && '180px'};
}
`;
export const StyledCoursesSelectHeaderWrapper = styled.div`
z-index: 2;
display: flex;
justify-content: space-between;
align-items: center;
height: 40px;
padding: 8px 15px;
background-color: ${({ customStyle }) => (customStyle ? BG_COLOR : MAIN1_DARK_COLOR)};
border-radius: 10px;
${HeaderAdaptiveFont};
p {
margin: 0;
font-weight: 400;
}
& > p {
@media (max-width: 440px) {
display: none;
}
}
svg {
transform: ${({ isClicked }) => (isClicked ? 'rotate(180deg)' : 'rotate(0deg)')};
path {
stroke: ${({ customStyle }) => customStyle && LIGHT_TEXT_COLOR};
}
}
`;
export const StyledCoursesList = styled.ul`
display: flex;
flex-direction: column;
width: 100%;
margin: 0;
margin-top: 0;
padding: 8px 10px;
transition: all 0.7s ease-in-out;
gap: 5px;
${HeaderAdaptiveFont}
li {
padding: 5px;
list-style: none;
background-color: ${({ customStyle }) => (customStyle ? WHITE_COLOR : MAIN1_COLOR)};
border-radius: 10px;
cursor: pointer;
&:hover {
background-color: transparent;
}
}
`;
export const StyledCoursesSelectInfo = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
width: ${({ isLang }) => isLang && '100%'};
&:hover {
cursor: ${({ hover }) => (hover ? 'pointer' : 'unset')};
}
p {
overflow: hidden;
max-width: 155px;
margin-left: 5px;
margin-right: ${({ hover }) => (hover ? '10px' : '31px')};
font-weight: 500;
white-space: nowrap;
text-overflow: ellipsis;
@media (max-width: 440px) {
margin-left: 0;
margin-right: ${({ hover }) => (hover ? '10px' : '0')};
}
}
svg {
${SVGArrowAdaptive};
}
@media (max-width: 440px) {
width: ${({ isLang }) => !isLang && '100%'};
}
`;
export const StyledCoursesSelectArrow = styled(CoursesSelectArrow)`
transition: transform 0.3s ease-in-out;
`;
================================================
FILE: src/components/CourseField/index.tsx
================================================
import React, { FC, SelectHTMLAttributes } from 'react';
import { Label, Select, SelectInner } from 'typography';
import { FieldWrapper, SelectCourse } from './styled';
import { ValidationAlert } from '../InputField/styled';
import { useTranslation } from 'react-i18next';
type Course = {
id: string;
name: string;
};
interface SelectFieldProps extends SelectHTMLAttributes {
labelText?: string;
placeholder: string;
multi?: boolean;
register: any;
courses: Course[];
onAdd?: any;
isValid?: boolean;
}
export const CourseField: FC = ({
labelText,
placeholder,
register,
courses,
onAdd,
isValid,
...rest
}) => {
const { t } = useTranslation();
const courseOptions = courses
? courses.map((course: Course) => {
return (
);
})
: null;
return (
{labelText && }
{!isValid && {t('You need to choose at least one course')}}
);
};
================================================
FILE: src/components/CourseField/styled.ts
================================================
import styled from 'styled-components';
import { BG_COLOR, LIGHT_TEXT_COLOR, MAIN1_COLOR, WHITE_COLOR } from 'appConstants/colors';
import { SVGParamsAdaptive } from 'typography';
type TPlusButton = {
active?: boolean;
};
export const FieldWrapper = styled.div`
display: flex;
flex-direction: column;
margin-bottom: 20px;
`;
export const SelectCourse = styled.div`
width: 300px;
margin-bottom: 0;
display: flex;
flex-direction: row;
justify-content: space-between;
@media (max-width: 440px) {
width: 100%;
}
`;
export const CourseButton = styled.button`
width: 40px;
height: 40px;
margin-left: 10px;
padding: 10px;
outline: none;
border-radius: 10px;
border: none;
cursor: pointer;
svg {
${SVGParamsAdaptive};
}
`;
export const PlusButton = styled(CourseButton)`
background-color: ${({ active }) => (active ? MAIN1_COLOR : BG_COLOR)};
path {
stroke: ${({ active }) => (active ? WHITE_COLOR : LIGHT_TEXT_COLOR)};
}
`;
export const CrossButton = styled(CourseButton)`
background: ${BG_COLOR}
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")
no-repeat center center;
`;
export const PlaceholderOption = styled.option`
display: none;
`;
================================================
FILE: src/components/ErrorBoundary/index.tsx
================================================
import React, { Component } from 'react';
import { BoundryContainer } from './styled';
interface ErrorBoundaryState {
error: any;
errorInfo: any;
}
class ErrorBoundary extends Component {
state = { error: null, errorInfo: null };
static getDerivedStateFromError(error: any, errorInfo: any) {
return { error: error, errorInfo: errorInfo };
}
componentDidCatch(error: any, errorInfo: any) {
// Catch errors in any components below and re-render with error message
this.setState({
error: error,
errorInfo: errorInfo,
});
// You can also log error messages to an error reporting service here
}
handleReload() {
location.reload();
}
render() {
const { children } = this.props;
if (this.state.errorInfo) {
return (
Something went wrong.
Click on the page to reload it.
);
}
return <>{children}>;
}
}
export default ErrorBoundary;
================================================
FILE: src/components/ErrorBoundary/styled.ts
================================================
import styled from 'styled-components';
import { PageTitle } from 'typography';
export const BoundryContainer = styled(PageTitle)`
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
font-size: 30px;
text-align: center;
`;
================================================
FILE: src/components/ErrorModal/index.tsx
================================================
import React, { FC } from 'react';
import { Modal } from 'components';
import { ApolloError } from '@apollo/client';
import { UNAUTHORIZED_ERROR_MESSAGE } from 'appConstants';
type Props = {
title?: string;
text?: string;
text2?: string;
open?: boolean;
cancelText?: string;
isCrossIconVisible?: boolean;
error?: ApolloError;
};
export const ErrorModal: FC = ({
title = 'Something went wrong!',
text = 'Please, try again later.',
text2 = '@besovadevka or @MadaShindeInai',
open = true,
isCrossIconVisible = false,
cancelText = 'Ok',
error,
}) => {
const isUserUnauthorized = !!error?.graphQLErrors.find(
({ message }) => message === UNAUTHORIZED_ERROR_MESSAGE
);
if (isUserUnauthorized) {
return null;
}
const onClose = () => {
location.reload();
};
return (
);
};
================================================
FILE: src/components/FilterForm/filterFormFields.ts
================================================
import { TFilterForm } from 'types';
import { InputFieldProps } from '../../components/InputField';
export const filterFormFields: InputFieldProps[] = [
{
name: 'discord',
labelText: 'Discord',
placeholder: 'Enter discord name',
register: {
pattern: {
value: /^[A-Za-z0-9@#-_() ]+$/i,
message: 'This input is letters and digits only.',
},
maxLength: {
value: 30,
message: 'This input exceed maxLength.',
},
},
},
{
name: 'github',
labelText: 'GitHub',
placeholder: 'Enter github name',
register: {
pattern: {
value: /^[A-Za-z0-9-_ ]+$/i,
message: 'This input is letters and digits only.',
},
maxLength: {
value: 30,
message: 'This input exceed maxLength.',
},
},
},
{
name: 'location',
labelText: 'Location',
placeholder: 'Enter location',
register: {
pattern: {
value: /^[A-Za-z\- ]+$/i,
message: 'This input is letters only.',
},
maxLength: {
value: 30,
message: 'This input exceed maxLength.',
},
},
},
{
name: 'courseName',
labelText: 'Course',
placeholder: 'Enter course name',
register: {
pattern: {
value: /^[A-Za-z\- ]+$/i,
message: 'This input is letters only.',
},
maxLength: {
value: 30,
message: 'This input exceed maxLength.',
},
},
},
];
export const filterSelectFields: [string, [string, string | boolean][], string, string][] = [
[
'Sort by score',
[
['Max score', 'DESC'],
['Min score', 'ASC'],
],
'100%',
'sortingOrder',
],
[
'Sort by team',
[
['All', false],
['Without team', true],
],
'100%',
'teamFilter',
],
];
export const defaultFilterData: TFilterForm = {
discord: null,
github: null,
location: null,
courseName: null,
sortingOrder: filterSelectFields[0][1][0][0],
teamFilter: filterSelectFields[1][1][0][0],
};
================================================
FILE: src/components/FilterForm/index.tsx
================================================
import React, { FC } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { FieldError, FieldErrors } from 'react-hook-form';
import { FilterSelect, InputField } from 'components';
import { InputsWrapper } from 'modules/EditProfile/styled';
import { TFilterForm } from 'types';
import { FilterFormBase, FilterButtonsWrapper, FilterButton } from './styled';
import { filterFormFields, filterSelectFields, defaultFilterData } from './filterFormFields';
import { DARK_TEXT_COLOR } from 'appConstants/colors';
import { Button } from 'typography';
import crossIcon from 'assets/svg/cross.svg';
import { selectFilterData } from 'modules/StudentsTable/selectors';
import { useTranslation } from 'react-i18next';
import { setFilterData } from 'modules/StudentsTable/studentsTableReducer';
type TFilter = {
inputValues: TFilterForm;
setInputValues: (data: TFilterForm) => void;
setIsFilterOpen: (data: boolean) => void;
setPage: (page: number) => void;
register: any;
handleSubmit: any;
errors: FieldErrors;
reset: any;
};
export const FilterForm: FC = ({
inputValues,
setInputValues,
setIsFilterOpen,
setPage,
register,
handleSubmit,
errors,
reset,
}) => {
const filterData = useSelector(selectFilterData);
const dispatch = useDispatch();
const { t } = useTranslation();
const changeInputValue = (e: React.ChangeEvent): void => {
const { name, value } = e.target;
setInputValues({
...inputValues,
[name]: value.trim(),
});
};
const onFilterFormSubmit = (e: React.FormEvent) => {
e.preventDefault();
};
const isValuesInnerEqual =
Object.values(defaultFilterData).toString() !== Object.values(inputValues).toString();
const isValuesOuterEqual =
Object.values(filterData).toString() !== Object.values(inputValues).toString();
return (
{filterSelectFields.map((item: [string, [string, string | boolean][], string, string]) => {
return (
it[0])}
onChange={changeInputValue}
currentOption={inputValues[item[3] as keyof TFilterForm] as string}
color={DARK_TEXT_COLOR}
/>
);
})}
{filterFormFields.map((item) => {
return (
);
})}
{isValuesInnerEqual && (
{
reset(defaultFilterData);
setInputValues(defaultFilterData);
}}
>
{
}
{t('Clear filter')}
)}
);
};
================================================
FILE: src/components/FilterForm/styled.ts
================================================
import styled from 'styled-components';
import { MAIN1_COLOR } from 'appConstants/colors';
import { EditProfileWrapper } from 'modules/EditProfile/styled';
import { Button } from 'typography';
type TFilerButtonProps = {
bgColor?: string | undefined;
clearBtn?: boolean;
outerBtn?: boolean;
};
export const FilterFormBase = styled(EditProfileWrapper)`
z-index: 2;
position: absolute;
top: 25px;
right: 0;
display: flex;
flex-direction: column;
justify-content: space-between;
height: 412px;
@media screen and (max-width: 768px) {
height: auto;
}
@media (max-width: 440px) {
top: 5px;
right: -13px;
width: 320px;
}
`;
export const FilterButton = styled(Button)`
background-color: ${({ bgColor }) => bgColor ?? 'transparent'};
color: ${MAIN1_COLOR};
display: flex;
align-items: center;
gap: ${({ clearBtn = false }) => (clearBtn ? '11px' : '20px')};
margin-left: ${({ outerBtn = false }) => (outerBtn ? 'auto' : '0')};
padding: ${({ clearBtn = false }) => (clearBtn ? '13px 20px 13px 0' : '20px 30px')};
@media (max-width: 1200px) {
padding: ${({ clearBtn = false }) => (clearBtn ? '12px 16px 12px 0' : '18px 28px')};
gap: ${({ clearBtn = false }) => (clearBtn ? '10px' : '18px')};
}
@media (max-width: 992px) {
padding: ${({ clearBtn = false }) => (clearBtn ? '13px 12px 13px 0' : '14px 26px')};
gap: ${({ clearBtn = false }) => (clearBtn ? '10px' : '17px')};
}
@media (max-width: 768px) {
padding: ${({ clearBtn = false }) => (clearBtn ? '13px 8px 13px 0' : '8px 26px')};
gap: ${({ clearBtn = false }) => (clearBtn ? '10px' : '16px')};
}
@media (max-width: 550px) {
padding: ${({ clearBtn = false }) => (clearBtn ? '11px 5px 11px 0' : '5px 25px')};
gap: ${({ clearBtn = false }) => (clearBtn ? '8px' : '13px')};
}
@media (max-width: 440px) {
padding: ${({ clearBtn = false }) => (clearBtn ? '9px 5px 9px 0' : '5px 20px')};
gap: ${({ clearBtn = false }) => (clearBtn ? '6px' : '10px')};
}
img {
filter: invert(100%) sepia() saturate(10000%) hue-rotate(-110deg);
@media (max-width: 992px) {
width: 15px;
}
@media (max-width: 768px) {
width: 14px;
}
@media (max-width: 550px) {
width: 12px;
}
@media (max-width: 440px) {
width: 10px;
}
}
`;
export const FilterButtonsWrapper = styled.div`
width: 100%;
display: flex;
align-items: center;
.SecondButtonForm {
margin-left: auto;
}
`;
================================================
FILE: src/components/FilterSelect/index.tsx
================================================
import React, { FC, SelectHTMLAttributes } from 'react';
import { Label, Select, SelectInner } from 'typography';
import { FieldWrapper, SelectCourse } from 'components/CourseField/styled';
import { useTranslation } from 'react-i18next';
interface SelectFieldProps extends SelectHTMLAttributes {
labelText?: string;
placeholder: string;
register: any;
options: string[];
currentOption: string;
}
export const FilterSelect: FC = ({
labelText,
placeholder,
register,
options,
currentOption,
...rest
}) => {
const { t } = useTranslation();
const filterFieldOptions =
options.map((option: string) => {
return (
);
}) ?? null;
return (
{labelText && }
);
};
================================================
FILE: src/components/Footer/components/FooterContent/index.tsx
================================================
import React, { FC } from 'react';
import { FooterContentBlock, FooterContentWrapper, FooterTitle } from 'components/Footer/styled';
import { FOOTER_INFO, LINK_TO_DESIGN_BLOCK } from 'appConstants';
import { useTranslation } from 'react-i18next';
export const FooterContent: FC = () => {
const { t } = useTranslation();
return (
{FOOTER_INFO.map((item, index) => {
return (
{t(item.title)}
{item.members.map((item: string) => {
return (
{item}
);
})}
);
})}
);
};
================================================
FILE: src/components/Footer/components/index.ts
================================================
export { FooterContent } from './FooterContent';
================================================
FILE: src/components/Footer/index.tsx
================================================
import React, { FC } from 'react';
import { RSLogo } from 'typography';
import { StyledFooter, FooterWrapper, FooterContentBlockLogo } from './styled';
import { Link } from 'react-router-dom';
import { FooterContent } from './components';
export const Footer: FC = () => {
return (
);
};
================================================
FILE: src/components/Footer/styled.ts
================================================
import styled from 'styled-components';
import { DARK_TEXT_COLOR, FOOTER_NAMES_COLOR, WHITE_COLOR } from 'appConstants/colors';
import { GeneralAdaptiveFont } from 'typography';
export const StyledFooter = styled.footer`
position: sticky;
top: 100%;
z-index: 2;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 110px;
padding: 0 4.2%;
background-color: ${DARK_TEXT_COLOR};
@media (max-width: 992px) {
height: 100px;
}
@media (max-width: 880px) {
height: 80px;
}
@media (max-width: 768px) {
height: 65px;
}
@media (max-width: 550px) {
height: 60px;
}
@media (max-width: 440px) {
height: 50px;
}
`;
export const FooterWrapper = styled.div`
display: flex;
justify-content: space-between;
width: 100%;
max-width: 1320px;
height: 100%;
`;
export const FooterContentWrapper = styled.div`
display: flex;
justify-content: flex-end;
width: 100%;
gap: 5.6%;
@media (max-width: 880px) {
display: none;
}
`;
export const FooterContentBlock = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
gap: 13px;
.contentBlock {
display: flex;
flex-wrap: wrap;
min-height: 24px;
gap: 30px;
@media (max-width: 992px) {
gap: 20px;
}
.contentItem {
height: 100%;
margin-bottom: -20px;
font: 400 1rem/24px 'Poppins', sans-serif;
color: ${FOOTER_NAMES_COLOR};
text-decoration: none;
outline: none;
${GeneralAdaptiveFont};
&:hover {
color: ${WHITE_COLOR};
}
@media (max-width: 992px) {
margin-bottom: 15px;
}
}
}
.contentBlock.designBlock {
width: auto;
}
.contentItem.designItem {
width: 140px;
}
`;
export const FooterContentBlockLogo = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
a {
height: 100%;
display: flex;
align-items: center;
}
`;
export const FooterTitle = styled.h1`
font: 600 1rem/24px 'Poppins', sans-serif;
color: ${WHITE_COLOR};
margin: 0;
${GeneralAdaptiveFont};
`;
================================================
FILE: src/components/Header/components/BurgerMenu/index.tsx
================================================
import React, { FC } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { NavLink } from 'react-router-dom';
import { selectIsBurgerMenuOpen } from 'modules/LoginPage/selectors';
import {
BurgerMenuWrapper,
BurgerMenuLayout,
CrossButton,
BurgerMenuNavList,
BurgerMenuNavListItem,
BurgerMenuOverlay,
} from './styled';
import { setBurgerMenuOpen } from 'modules/LoginPage/loginPageReducer';
import { APP_NAVIGATION_LINKS } from 'appConstants';
import { useTranslation } from 'react-i18next';
import { TNavLink } from 'types';
import { LangSelect } from '../MenuWrapper/components/LangSelect';
type BurgerMenuProps = {
newUserCheck: boolean;
navOnClickHandler: (e: React.MouseEvent, path: string) => void;
};
export const BurgerMenu: FC = ({ newUserCheck, navOnClickHandler }) => {
const dispatch = useDispatch();
const { t } = useTranslation();
const isBurgerMenuOpen = useSelector(selectIsBurgerMenuOpen);
const onClickMenuToggle = () => {
dispatch(setBurgerMenuOpen(!isBurgerMenuOpen));
};
return (
{Object.values(APP_NAVIGATION_LINKS).map((link: TNavLink, index: number) => {
if (+newUserCheck + +link.isAlwaysVisible) {
return (
{
onClickMenuToggle();
navOnClickHandler(e, Object.keys(APP_NAVIGATION_LINKS)[index]);
}}
>
{t(link.name)}
);
}
})}
);
};
================================================
FILE: src/components/Header/components/BurgerMenu/styled.ts
================================================
import styled from 'styled-components';
import { DARK_TEXT_COLOR, OVERLAY_COLOR, WHITE_COLOR } from 'appConstants/colors';
import { ReactComponent as IconClose } from 'assets/svg/cross.svg';
type BurgerMenuProps = {
isBurgerMenuOpen: boolean;
};
export const BurgerMenuWrapper = styled.div`
position: fixed;
z-index: 3;
top: 0;
display: flex;
width: 100vw;
height: 100vh;
transition: transform 0.6s ease-in-out;
transform: ${({ isBurgerMenuOpen }) => (isBurgerMenuOpen ? 'translateX(0)' : 'translateX(100%)')};
`;
export const BurgerMenuOverlay = styled.div`
width: calc(100% - 250px);
height: 100%;
background-color: ${({ isBurgerMenuOpen }) => (isBurgerMenuOpen ? OVERLAY_COLOR : 'none')};
transition: background-color 0.6s ease-in-out;
@media (max-width: 440px) {
width: calc(100% - 180px);
}
`;
export const BurgerMenuLayout = styled.nav`
display: flex;
flex-direction: column;
gap: 40px;
width: 250px;
height: 100%;
padding: 20px 20px 20px 40px;
background: ${WHITE_COLOR};
@media (max-width: 440px) {
width: 180px;
padding-left: 20px;
}
`;
export const CrossButton = styled(IconClose)`
width: 16px;
height: 16px;
align-self: flex-end;
cursor: pointer;
`;
export const BurgerMenuNavList = styled.ul`
display: flex;
flex-direction: column;
gap: 40px;
margin: 0;
padding-inline-start: 0;
`;
export const BurgerMenuNavListItem = styled.li`
list-style-type: none;
a {
text-decoration: none;
color: ${DARK_TEXT_COLOR};
}
a.activeNavLink,
a:hover {
font-weight: 700;
}
`;
================================================
FILE: src/components/Header/components/MenuWrapper/components/CoursesSelect/index.tsx
================================================
import React, { FC, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { selectCurrCourse } from 'modules/LoginPage/selectors';
import { selectUserData } from 'modules/StudentsTable/selectors';
import { Course } from 'types';
import { CommonSelectList } from 'components';
import { setCourse } from 'modules/LoginPage/loginPageMiddleware';
export const CoursesSelect: FC = () => {
const [isCourseSelectOpen, setCourseSelectOpen] = useState(false);
const dispatch = useDispatch();
const currCourse = useSelector(selectCurrCourse);
const userData = useSelector(selectUserData);
const userCourses = userData.courses.filter((item) => item.id !== currCourse.id) ?? null;
const onCourseChange = (course: Course) => {
setCourseSelectOpen(false);
dispatch(setCourse(course));
};
return (
);
};
================================================
FILE: src/components/Header/components/MenuWrapper/components/LangSelect/index.tsx
================================================
import React, { FC, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { LANGUAGES } from 'appConstants';
import { selectCurrLanguage } from 'modules/LoginPage/selectors';
import i18n from 'translation/resources';
import { CommonSelectList } from 'components';
import { setLanguage } from 'modules/LoginPage/loginPageMiddleware';
type LangSelectProps = {
menuToggle?: boolean;
customStyle?: boolean;
};
export const LangSelect: FC = ({ menuToggle, customStyle }) => {
const [displayLangList, setDisplayLangList] = useState(false);
const dispatch = useDispatch();
const currentLanguage = useSelector(selectCurrLanguage);
const onLangChange = (item: string) => {
setDisplayLangList(false);
dispatch(setLanguage(item));
i18n.changeLanguage(item);
};
const languages: string[] = LANGUAGES.filter((lang) => lang !== currentLanguage);
return (
);
};
================================================
FILE: src/components/Header/components/MenuWrapper/components/Nav/index.tsx
================================================
import React, { FC } from 'react';
import { useTranslation } from 'react-i18next';
import { APP_NAVIGATION_LINKS } from 'appConstants';
import { NavLink } from 'react-router-dom';
import { StyledNav, StyledNavList, StyledNavListItem, StyledHeaderActiveElement } from './styled';
import { TNavLink } from 'types';
type NavProps = {
newUserCheck: boolean;
navOnClickHandler: (e: React.MouseEvent, path: string) => void;
isUserAdmin: boolean;
};
export const Nav: FC = ({ newUserCheck, navOnClickHandler, isUserAdmin }) => {
const { t } = useTranslation();
return (
{Object.values(APP_NAVIGATION_LINKS).map((link: TNavLink, index: number) => {
const isNavLinkAvailable = !!(+newUserCheck + +link.isAlwaysVisible);
if (isNavLinkAvailable) {
if (link.name === 'Admin' && !isUserAdmin) return null;
return (
navOnClickHandler(e, Object.keys(APP_NAVIGATION_LINKS)[index])}
>
{t(link.name)}
);
}
})}
);
};
================================================
FILE: src/components/Header/components/MenuWrapper/components/Nav/styled.ts
================================================
import { WHITE_COLOR } from 'appConstants/colors';
import styled, { keyframes } from 'styled-components';
import { ReactComponent as HeaderActiveElement } from 'assets/svg/headerActiveElement.svg';
type TStyledNavListItemProps = {
newUserCheck: boolean;
};
const open = keyframes`
from: { height: 0 }
to: { height: 100%}
`;
export const StyledNav = styled.nav`
display: flex;
justify-content: center;
align-items: center;
align-self: flex-end;
margin-right: 60px;
font: 400 1rem/24px 'Poppins', sans-serif;
`;
export const StyledNavList = styled.ul`
@media (max-width: 1430px) {
display: none;
}
display: flex;
justify-content: center;
align-items: center;
margin: 0;
padding-inline-start: 0;
gap: 60px;
`;
export const StyledHeaderActiveElement = styled(HeaderActiveElement)`
width: 87px;
height: 0;
`;
export const StyledNavListItem = styled.li`
list-style-type: none;
a {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
height: 52px;
color: ${WHITE_COLOR};
text-decoration: none;
&:hover {
font-weight: 700;
}
svg {
height: 0;
margin-bottom: -5px;
transition: all 0.5s ease-in-out;
animation: ${open} 0.5s ease-in-out;
}
}
a.activeNavLink {
font-weight: 700;
svg {
height: 22px;
margin-bottom: 0;
}
}
`;
================================================
FILE: src/components/Header/components/MenuWrapper/index.tsx
================================================
import React, { FC } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { selectUserData } from 'modules/StudentsTable/selectors';
import { CoursesSelect } from './components/CoursesSelect';
import { Nav } from './components/Nav';
import { MenuButton, StyledMenuWrapper } from './styled';
import { LangSelect } from './components/LangSelect';
import { setBurgerMenuOpen } from 'modules/LoginPage/loginPageReducer';
import { selectIsBurgerMenuOpen } from 'modules/LoginPage/selectors';
type MenuWrapperProps = {
navOnClickHandler: (e: React.MouseEvent, path: string) => void;
};
export const MenuWrapper: FC = ({ navOnClickHandler }) => {
const dispatch = useDispatch();
const userData = useSelector(selectUserData);
const isBurgerMenuOpen = useSelector(selectIsBurgerMenuOpen);
const newUserCheck = !!userData?.courses.length;
const isUserAdmin = !!userData?.isAdmin;
const onClickMenuToggle = () => {
dispatch(setBurgerMenuOpen(!isBurgerMenuOpen));
};
return (
{newUserCheck && }
);
};
================================================
FILE: src/components/Header/components/MenuWrapper/styled.ts
================================================
import styled from 'styled-components';
import { ReactComponent as MenuToggle } from 'assets/svg/menuToggle.svg';
export const StyledMenuWrapper = styled.div`
display: flex;
height: 60px;
@media (max-width: 1260px) {
height: 40px;
}
@media (max-width: 550px) {
margin-left: -30px;
}
@media (max-width: 440px) {
margin-left: -50px;
}
`;
export const MenuButton = styled(MenuToggle)`
width: 0;
height: 0;
cursor: pointer;
@media (max-width: 1430px) {
width: 24px;
height: 24px;
margin: 8px 0 0 40px;
}
@media (max-width: 700px) {
width: 20px;
height: 20px;
margin: 10px 0 0 30px;
}
@media (max-width: 600px) {
margin-left: 0;
}
@media (max-width: 440px) {
width: 16px;
height: 16px;
margin-top: 12px;
}
`;
================================================
FILE: src/components/Header/components/index.ts
================================================
export { MenuWrapper } from './MenuWrapper';
export { BurgerMenu } from './BurgerMenu';
================================================
FILE: src/components/Header/index.tsx
================================================
import React, { FC } from 'react';
import { Link } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import { RSLogo } from 'typography';
import { StyledHeader, Container } from './styled';
import { selectIsEditProfileDataChange, selectToken } from 'modules/LoginPage/selectors';
import { MenuWrapper, BurgerMenu } from './components';
import { selectUserData } from 'modules/StudentsTable/selectors';
import { activeModalLeavePage } from 'modules/TeamsList/teamsListReducer';
import { setPathToThePage } from 'modules/LoginPage/loginPageReducer';
export const Header: FC = () => {
const dispatch = useDispatch();
const userData = useSelector(selectUserData);
const loginToken = useSelector(selectToken);
const isEditProfileDataChange = useSelector(selectIsEditProfileDataChange);
const newUserCheck = !!userData?.courses.length;
const navOnClickHandler = (e: React.MouseEvent, path: string) => {
if (isEditProfileDataChange) {
e.preventDefault();
dispatch(activeModalLeavePage(true));
dispatch(setPathToThePage(path));
}
};
return (
<>
{loginToken && }
>
);
};
================================================
FILE: src/components/Header/styled.ts
================================================
import styled from 'styled-components';
import { MAIN1_COLOR } from 'appConstants/colors';
import { GeneralAdaptiveFont } from 'typography';
type TStyledHeaderProps = {
login: string | null;
};
export const StyledHeader = styled.header`
position: sticky;
display: flex;
justify-content: center;
align-items: flex-end;
height: 80px;
padding: ${({ login }) => (login ? '1.4% 4.2% 0' : '2.8% 4.2% 0 2.8%')};
background-color: ${({ login }) => (login ? MAIN1_COLOR : 'transparent')};
width: 100%;
z-index: 1;
@media (max-width: 1260px) {
align-items: center;
}
`;
export const Container = styled.div`
display: flex;
justify-content: space-between;
width: 100%;
max-width: 1320px;
${GeneralAdaptiveFont};
`;
================================================
FILE: src/components/InputField/index.tsx
================================================
import React, { FC, InputHTMLAttributes } from 'react';
import { Input } from 'typography';
import { FieldWrapper, ValidationAlert, FLabel } from './styled';
import { useTranslation } from 'react-i18next';
export interface InputFieldProps extends InputHTMLAttributes {
labelText?: string;
placeholder?: string;
register?: any;
name: string;
message?: string | undefined;
onChange?: (e: React.ChangeEvent) => void;
}
export const InputField: FC = ({
labelText,
placeholder,
register,
name,
message,
onChange,
}) => {
const { t } = useTranslation();
return (
{labelText ? t(labelText) : ''}
{message ? t(message) : ''}
);
};
================================================
FILE: src/components/InputField/styled.ts
================================================
import styled from 'styled-components';
import { ALERT_COLOR } from 'appConstants/colors';
import { GeneralAdaptiveFont, Label } from 'typography';
export const FieldWrapper = styled.div`
display: flex;
flex-direction: column;
margin-bottom: 0;
`;
export const ValidationAlert = styled.div`
${GeneralAdaptiveFont}
color: ${ALERT_COLOR};
font-size: 14px;
height: 22px;
`;
export const FLabel = styled(Label)`
margin-bottom: 8px;
`;
================================================
FILE: src/components/Loader/index.tsx
================================================
import React, { FC } from 'react';
import { LoaderStyled, LoaderWrapper } from './styled';
export const Loader: FC = () => {
return (
);
};
================================================
FILE: src/components/Loader/styled.ts
================================================
import styled from 'styled-components';
import { MAIN1_COLOR } from 'appConstants/colors';
import { MainComponentHeight } from 'typography';
export const LoaderWrapper = styled.div`
position: absolute;
top: 50%;
left: 50%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
transform: translate(-50%, -50%);
${MainComponentHeight};
`;
export const LoaderStyled = styled.div`
width: 25%;
.loader {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 2.5em;
gap: 12px;
animation-duration: 1.4s;
animation-delay: 0.32s;
margin: auto;
text-align: center;
@media (max-width: 440px) {
gap: 9px;
}
.loader-child {
width: 1.8em;
height: 1.8em;
background-color: ${MAIN1_COLOR};
border-radius: 100%;
display: inline-block;
animation: loader-in 1.6s ease-in-out 0s infinite both;
@media (max-width: 1200px) {
width: 1.7em;
height: 1.7em;
}
@media (max-width: 550px) {
width: 1.4em;
height: 1.4em;
}
@media (max-width: 440px) {
width: 1em;
height: 1em;
}
}
.loader-child-1 {
animation-delay: -0.6s;
}
.loader-child-2 {
animation-delay: -0.4s;
}
.loader-child-3 {
animation-delay: -0.2s;
}
}
@keyframes loader-in {
0%,
80%,
100% {
transform: scale(0);
}
40% {
transform: scale(1);
}
}
`;
================================================
FILE: src/components/Modal/index.module.css
================================================
/* setting absolute can break other modals */
.overlay {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 999;
text-align: center;
background: var(--OVERLAY_COLOR);
overscroll-behavior: contain;
}
/* setting max width to container can break other modals */
.container {
position: relative;
display: flex;
flex-direction: column;
width: 440px;
margin: 11vh auto;
padding: 30px 40px;
text-align: center;
background: var(--WHITE_COLOR);
border: none;
border-radius: 20px;
}
.icon {
position: absolute;
top: 20px;
right: 20px;
z-index: 1;
width: 25px;
height: 25px;
cursor: pointer;
}
@media (max-width: 550px) {
.container {
width: 350px;
}
.icon {
top: 15px;
right: 20px;
width: 22px;
height: 22px;
}
}
@media (max-width: 440px) {
.container {
width: 300px;
}
.icon {
top: 15px;
right: 20px;
width: 20px;
height: 20px;
}
}
================================================
FILE: src/components/Modal/index.tsx
================================================
import React, { FC, SyntheticEvent, useEffect, useRef } from 'react';
import ReactDOM from 'react-dom';
import { ReactComponent as IconClose } from 'assets/svg/cross.svg';
import styled from 'styled-components';
import styles from './index.module.css';
import { PageTitle, Label, Button, InvertedButton, ButtonsBlock } from 'typography';
import { useTranslation } from 'react-i18next';
type ModalProps = {
title: string;
text: string;
text2?: string;
open: boolean;
hideOnOutsideClick?: boolean;
hideOnEsc?: boolean;
children?: React.ReactNode;
onClose(): void;
onSubmit?: (e: SyntheticEvent) => void;
okText?: string;
cancelText?: string;
isCrossIconVisible?: boolean;
} & typeof defaultProps;
const defaultProps = {
hideOnOutsideClick: true,
hideOnEsc: true,
};
const ModalWindow = styled.div`
left: 500px;
top: 315px;
background: #ffffff;
border-radius: 20px;
`;
export const Modal: FC = ({
title,
text,
text2,
open,
children,
hideOnOutsideClick,
hideOnEsc,
onClose,
onSubmit,
isCrossIconVisible = true,
okText,
cancelText,
}) => {
const insideRef = useRef(null);
const { t } = useTranslation();
const close = (e: React.MouseEvent | MouseEvent) => {
e.stopPropagation();
onClose();
};
const onOutClick: React.MouseEventHandler = (e) => {
const curRef = insideRef.current;
if (hideOnOutsideClick && curRef && !curRef.contains(e.target as Node)) {
onClose();
}
};
useEffect(() => {
const listener = (e: KeyboardEvent) => {
if (e.key === 'Escape' || (!onSubmit && e.key === 'Enter')) {
onClose();
}
};
if (open && hideOnEsc) {
document.addEventListener('keydown', listener);
}
return () => document.removeEventListener('keydown', listener);
}, [open, onClose, hideOnEsc, onSubmit]);
useEffect(() => {
if (open) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
}
return () => {
document.body.style.overflow = '';
};
}, [open]);
if (!open) {
return null;
}
return ReactDOM.createPortal(
{isCrossIconVisible &&
}
{t(title)}
{children}
{onSubmit ? (
cancelText ? (
<>
{t(cancelText)}
>
) : (
)
) : (
)}
,
document.body
);
};
Modal.defaultProps = defaultProps;
================================================
FILE: src/components/ModalCreateEditTeam/index.tsx
================================================
import React, { FC, useState, useEffect, useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { Modal } from 'components';
import { ModalInput } from 'typography';
import { ValidationAlert } from '../InputField/styled';
import { useTranslation } from 'react-i18next';
import { setSocialLink } from 'modules/TeamsList/teamsListReducer';
import { isFieldValid } from 'utils/isFieldValid';
type Props = {
title: string;
text: string;
open: boolean;
onSubmit?: () => void;
onClose: () => void;
value: string;
okText?: string;
validateRules?: any;
} & typeof defaultProps;
const defaultProps = {
open: false,
okText: 'Create team',
};
export const ModalCreateEditTeam: FC = ({
title,
text,
open,
okText,
value,
onClose,
onSubmit,
validateRules,
}) => {
const dispatch = useDispatch();
const { t } = useTranslation();
const [isInputValid, setInputValid] = useState(false);
const [errorMessage, setErrorMessage] = useState('');
const onSubmitModal = useCallback(() => {
if (isInputValid && onSubmit) {
onSubmit();
onClose();
} else {
setErrorMessage('Please, enter link');
}
}, [onClose, onSubmit, isInputValid]);
const onChangeModal = (e: React.ChangeEvent) => {
dispatch(setSocialLink(e.target.value.trim()));
isFieldValid(
e.target.value.trim(),
validateRules,
!!validateRules,
setInputValid,
setErrorMessage
);
};
const onCloseModal = () => {
onClose();
setErrorMessage('');
};
useEffect(() => {
const listener = (e: KeyboardEvent) => {
if (e.key === 'Enter') {
onSubmitModal();
}
};
if (open) {
document.addEventListener('keydown', listener);
}
return () => document.removeEventListener('keydown', listener);
}, [onSubmitModal, open]);
return (
{!isInputValid && {t(errorMessage)}}
);
};
================================================
FILE: src/components/ModalCreated/index.tsx
================================================
import React, { FC, useState } from 'react';
import styled from 'styled-components';
import { Modal } from 'components';
import { ModalInput, InvertedButton } from 'typography';
import { BG_COLOR } from 'appConstants/colors';
import CopyIcon from 'assets/svg/copy2clip.svg';
const InputWithCopy = styled.div`
position: relative;
`;
const CopyButton = styled(InvertedButton)`
right: 40px;
bottom: 0;
padding: 19px 15px;
position: absolute;
background: ${BG_COLOR} url(${CopyIcon}) no-repeat center center;
`;
type Props = {
title: string;
text: string;
text2?: string;
open: boolean;
onClose: () => void;
cancelText?: string;
password: string;
} & typeof defaultProps;
const defaultProps = {
text2: '',
};
export const ModalCreated: FC = ({
title,
text,
text2,
open,
cancelText,
onClose,
password,
}) => {
const [isCopy, setIsCopy] = useState(false);
const CopyToClipboard = (text: string) => {
navigator.clipboard
.writeText(text)
.then(() => {
setIsCopy(true);
setTimeout(() => {
setIsCopy(false);
}, 1000);
})
.catch((err) => {
console.log('Something went wrong', err);
});
};
const onClickCopyButton = () => CopyToClipboard(password);
return (
);
};
================================================
FILE: src/components/ModalEditCourse/index.tsx
================================================
import React, { FC, useState, useEffect, useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { Modal } from 'components';
import { TeamButton } from 'typography';
import { useTranslation } from 'react-i18next';
import { activeModalEditCourse } from 'modules/TeamsList/teamsListReducer';
import { InputBlock } from 'modules/AdminPage/components/ContentWrapper/components/AddCourseBlock/components/InputsBlock';
import { Course } from 'types';
import { COURSE_NAME_VALIDATION, TEAM_SIZE_VALIDATION } from 'appConstants';
import { onChangeField } from 'modules/AdminPage/components/ContentWrapper/components/AddCourseBlock';
import { DASHBOARD_HEADER_BG_COLOR, MAIN1_COLOR } from 'appConstants/colors';
import { useUpdateCourseMutation } from 'hooks/graphql';
import { ErrorModal } from 'components/ErrorModal';
type Props = {
title: string;
text: string;
open: boolean;
okText?: string;
cancelText?: string;
courseEditMeta: Course;
checkIsCourseNameUniq: (courseName: string) => boolean;
setCourseEditMeta: (course: Course | null) => void;
};
const isCourseInfoChanged = (
{ name, teamSize, isActive }: Course,
newName: string,
newTeamSize: string,
newStatus: boolean
) => name !== newName || `${teamSize}` !== newTeamSize || isActive !== newStatus;
export const ModalEditCourse: FC = ({
title,
text,
open,
okText,
cancelText,
courseEditMeta,
checkIsCourseNameUniq,
setCourseEditMeta,
}) => {
const { t } = useTranslation();
const dispatch = useDispatch();
const [isCourseNameFieldValid, setIsCourseNameFieldValid] = useState(true);
const [isTeamSizeFieldValid, setIsTeamSizeFieldValid] = useState(true);
const [courseName, setCourseName] = useState(courseEditMeta.name);
const [teamSize, setTeamSize] = useState(`${courseEditMeta.teamSize ?? ''}`);
const [isCourseActive, setIsCourseActive] = useState(courseEditMeta.isActive);
const [courseNameErrorMessage, setCourseNameErrorMessage] = useState('');
const [teamSizeErrorMessage, setTeamSizeErrorMessage] = useState('');
const { updateCourse, errorM: errorUpdateCourse } = useUpdateCourseMutation({
course: {
id: courseEditMeta?.id || '',
name: courseName,
teamSize: +teamSize,
isActive: isCourseActive,
},
});
const onCloseModal = useCallback(() => {
setCourseEditMeta(null);
setCourseNameErrorMessage('');
setTeamSizeErrorMessage('');
dispatch(activeModalEditCourse(false));
}, [dispatch, setCourseEditMeta]);
const onSubmitModal = useCallback(() => {
if (isCourseNameFieldValid && isTeamSizeFieldValid) {
if (courseName && teamSize) {
const isCourseChanged = courseEditMeta
? isCourseInfoChanged(courseEditMeta, courseName, teamSize, isCourseActive)
: true;
isCourseChanged && updateCourse();
onCloseModal();
} else {
if (!courseName) {
setIsCourseNameFieldValid(false);
setCourseNameErrorMessage(COURSE_NAME_VALIDATION.minLength.message);
}
if (!teamSize) {
setIsTeamSizeFieldValid(false);
setTeamSizeErrorMessage(TEAM_SIZE_VALIDATION.minLength.message);
}
}
}
}, [
onCloseModal,
courseEditMeta,
courseName,
isCourseActive,
isCourseNameFieldValid,
isTeamSizeFieldValid,
teamSize,
updateCourse,
]);
useEffect(() => {
const listener = (e: KeyboardEvent) => {
if (e.key === 'Enter') {
onSubmitModal();
}
};
if (open) {
document.addEventListener('keydown', listener);
}
return () => document.removeEventListener('keydown', listener);
}, [onSubmitModal, open]);
const changeCourseStateButtonText = isCourseActive ? 'Terminate course' : 'Activate course';
const onChangeCourseState = () => setIsCourseActive(!isCourseActive);
if (errorUpdateCourse) return ;
return (
{t(changeCourseStateButtonText)}
);
};
================================================
FILE: src/components/ModalExpel/index.tsx
================================================
import React, { FC, useEffect, useCallback } from 'react';
import { Modal } from 'components';
type Props = {
title: string;
text: string;
open: boolean;
onSubmit?: () => void;
onClose: () => void;
okText?: string;
isCrossIconVisible?: boolean;
cancelText?: string;
};
export const ModalExpel: FC = ({
title,
text,
open,
okText,
cancelText,
isCrossIconVisible = true,
onClose,
onSubmit,
}) => {
const onSubmitModal = useCallback(() => {
onClose();
if (onSubmit) {
onSubmit();
}
}, [onClose, onSubmit]);
useEffect(() => {
const listener = (e: KeyboardEvent) => {
if (e.key === 'Enter') {
onSubmitModal();
}
};
if (open) {
document.addEventListener('keydown', listener);
}
return () => document.removeEventListener('keydown', listener);
}, [onSubmitModal, open]);
return (
);
};
================================================
FILE: src/components/ModalJoin/index.tsx
================================================
import React, { FC, useCallback, useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { Modal } from 'components';
import { ModalInput } from 'typography';
import { useTranslation } from 'react-i18next';
import { setTeamPassword } from 'modules/TeamsList/teamsListReducer';
type Props = {
title: string;
text: string;
open: boolean;
okText?: string;
value: string;
cancelText?: string;
onClose: () => void;
onSubmit?: (e: string) => void;
onChange?: () => void;
};
export const ModalJoin: FC = ({
title,
text,
open,
okText,
value,
cancelText,
onClose,
onSubmit,
onChange,
}) => {
const dispatch = useDispatch();
const { t } = useTranslation();
const onSubmitModal = useCallback(() => {
if (onSubmit && value) {
onSubmit(value);
}
}, [value, onSubmit]);
const onChangeModal = (e: React.ChangeEvent) => {
if (onChange) {
onChange();
}
dispatch(setTeamPassword(e.target.value));
};
useEffect(() => {
const listener = (e: KeyboardEvent) => {
if (e.key === 'Enter') {
onSubmitModal();
}
};
if (open) {
document.addEventListener('keydown', listener);
}
return () => document.removeEventListener('keydown', listener);
}, [onSubmitModal, open]);
return (
);
};
================================================
FILE: src/components/Pagination/index.tsx
================================================
import React, { FC } from 'react';
import ReactPaginate from 'react-paginate';
import './style.css';
import leftArrow from 'assets/svg/paginateArrowLeft.svg';
import rightArrow from 'assets/svg/paginateArrowRight.svg';
type PaginationProps = {
pageCount: number;
changePage: (page: number) => void;
page: number;
};
export const Pagination: FC = ({ pageCount, changePage, page }) => {
return (
}
nextLabel={
}
breakLabel={'...'}
pageCount={pageCount}
initialPage={page}
marginPagesDisplayed={1}
pageRangeDisplayed={2}
containerClassName={'pagination'}
pageClassName={'pageContainer'}
pageLinkClassName={'pageLink'}
activeClassName={'activePageContainer'}
activeLinkClassName={'activePageLink'}
onPageChange={(page) => changePage(page.selected)}
/>
);
};
================================================
FILE: src/components/Pagination/style.css
================================================
.pagination {
display: flex;
justify-content: center;
align-items: center;
margin: 40px auto 0;
gap: 10px;
padding-inline-start: 0;
}
.pagination li {
list-style: none;
}
.pagination li a {
display: flex;
justify-content: center;
align-items: center;
width: 40px;
height: 40px;
padding: 8px;
outline: none;
}
.pageContainer,
.previous,
.next,
.break {
display: flex;
justify-content: center;
align-items: center;
background-color: #ffffff;
border-radius: 10px;
cursor: pointer;
}
.previous a img,
.next a img {
width: 12px;
height: 12px;
}
.previous.disabled,
.next.disabled {
cursor: unset;
opacity: 0.7;
}
.activePageContainer {
background-color: #6550f6;
cursor: unset;
}
.pageLink,
.break > a {
display: flex;
justify-content: center;
align-items: center;
font: 400 1rem/24px "Poppins", sans-serif;
color: #7e96c2;
text-decoration: none;
}
.activePageLink {
color: #ffffff;
}
@media (max-width: 1200px) and (min-width: 992px) {
.pagination {
margin: 35px auto 0;
}
.pageLink,
.break > a {
font-size: 0.95rem;
}
}
@media (max-width: 992px) {
.pagination {
margin: 30px auto 0;
}
.pageLink,
.break > a {
font-size: 0.9rem;
}
.pagination li a {
width: 35px;
height: 35px;
}
}
@media (max-width: 768px) {
.pagination {
margin: 30px auto 0;
}
.pageLink,
.break > a {
font-size: 0.825rem;
}
.pagination li a {
width: 30px;
height: 30px;
}
.previous a img,
.next a img {
width: 10px;
height: 10px;
}
}
@media (max-width: 550px) {
.pagination {
margin: 25px auto 0;
}
.pageLink,
.break > a {
font-size: 0.8rem;
}
.pagination li a {
width: 30px;
height: 30px;
}
.previous a img,
.next a img {
width: 10px;
height: 10px;
}
}
@media (max-width: 440px) {
.pagination {
margin: 20px auto 0;
}
.pageLink,
.break > a {
font-size: 0.68rem;
}
.pagination li a {
width: 25px;
height: 25px;
}
.previous a img,
.next a img {
width: 9px;
height: 9px;
}
}
================================================
FILE: src/components/PrivateRoute/index.tsx
================================================
import React, { FC } from 'react';
import { Redirect, Route } from 'react-router-dom';
type Props = {
isLoggedIn: boolean;
path: string;
component: FC;
exact?: boolean;
newUserCheck?: boolean;
};
export const PrivateRoute: FC = ({
component: Component,
isLoggedIn,
newUserCheck,
path,
exact,
}) => {
return (
{
if (isLoggedIn && newUserCheck) {
return ;
}
if (isLoggedIn && !newUserCheck) {
return ;
}
if (!isLoggedIn) {
return ;
}
}}
/>
);
};
================================================
FILE: src/components/SelectField/index.tsx
================================================
import React, { FC, SelectHTMLAttributes } from 'react';
import { Label, Select, SelectInner } from 'typography';
import { FieldWrapper } from './styled';
//
// for future
// TODO: make separate SelectField component for other screens
// now is not used
//
interface SelectFieldProps extends SelectHTMLAttributes {
labelText: string;
placeholder: string;
multi?: boolean;
register: any;
}
const SelectField: FC = ({ labelText, placeholder, register, ...rest }) => {
return (
);
};
================================================
FILE: src/components/SelectField/styled.ts
================================================
import styled from 'styled-components';
export const FieldWrapper = styled.div`
display: flex;
flex-direction: column;
margin-bottom: 20px;
`;
export const PlaceholderOption = styled.option`
display: none;
`;
================================================
FILE: src/components/TablePopup/index.tsx
================================================
import React, { FC } from 'react';
import { StyledPopup, StyledPopupItem } from './styled';
type TablePopupProps = {
dataLength: number;
popupElements: string[];
};
export const TablePopup: FC = ({ popupElements, dataLength }) => {
return (
{popupElements?.map((element: string) => (
{element}
))}
);
};
================================================
FILE: src/components/TablePopup/styled.ts
================================================
import { WHITE_COLOR, DARK_TEXT_COLOR, TABLE_POPUP_BORDER_COLOR } from 'appConstants/colors';
import styled from 'styled-components';
import { GeneralAdaptiveFont } from 'typography';
type TStyledPopup = {
dataLength: number;
};
export const StyledPopup = styled.div`
position: absolute;
top: ${({ dataLength }) => dataLength < 0.5 && '95%'};
bottom: ${({ dataLength }) => dataLength > 0.5 && '95%'};
right: 0;
z-index: 2;
display: flex;
flex-direction: column;
justify-content: center;
padding: 10px;
font-size: 1rem;
color: ${DARK_TEXT_COLOR};
background-color: ${WHITE_COLOR};
border: 1px solid ${TABLE_POPUP_BORDER_COLOR};
border-radius: 10px;
${GeneralAdaptiveFont};
@media (max-width: 440px) {
left: -5px;
padding: 5px;
}
`;
export const StyledPopupItem = styled.p`
margin: 0;
line-height: 17px;
text-align: justify;
`;
================================================
FILE: src/components/TourGuide/index.tsx
================================================
import React, { FC, useCallback, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { selectIsTourOpen } from 'modules/LoginPage/selectors';
import Tour, { ReactourStep } from 'reactour';
import { tourConfig } from './tourConfig';
import { setIsTourOpen } from 'modules/LoginPage/loginPageReducer';
import { MAIN1_COLOR } from 'appConstants/colors';
import leftArrow from 'assets/svg/paginateArrowLeft.svg';
import rightArrow from 'assets/svg/paginateArrowRight.svg';
import './style.css';
import { useHistory } from 'react-router';
import { useTranslation } from 'react-i18next';
export const TourGuide: FC = () => {
const [isButtonsVisible, setIsButtonsVisible] = useState(false);
const dispatch = useDispatch();
const isTourOpen = useSelector(selectIsTourOpen);
const history = useHistory();
const { t } = useTranslation();
const onRequestCloseHandler = () => dispatch(setIsTourOpen(false));
const tourConfigInfo = useCallback(
() => tourConfig(history, dispatch, t),
[history, dispatch, t]
);
return (
}
nextButton={
}
accentColor={MAIN1_COLOR}
maskClassName="mask"
disableDotsNavigation
disableKeyboardNavigation
showNavigationNumber={false}
showButtons={isButtonsVisible}
disableInteraction
lastStepNextButton={}
getCurrentStep={(currStep) => setIsButtonsVisible(!!currStep)}
closeWithMask={false}
className="helper"
rounded={20}
/>
);
};
================================================
FILE: src/components/TourGuide/style.css
================================================
span[data-tour-elem="badge"] {
top: 20px;
left: 20px;
width: 30px;
height: 30px;
padding: 0;
font-weight: bold;
font-family: "Poppins", sans-serif;
border-radius: 50%;
box-shadow: none;
}
.reactour__helper.helper {
width: 440px;
max-width: 440px;
padding: 64px 20px 20px;
font-family: "Poppins", sans-serif;
text-align: justify;
box-shadow: none;
}
.reactour__helper.helper p {
text-align: center;
}
.reactour__helper.helper > .reactour__close {
top: 18px;
right: 18px;
width: 18px;
height: 18px;
color: #7e96c2;
background: center / contain url("../../assets/svg/cross.svg") no-repeat;
}
.reactour__close > svg {
display: none;
}
button[data-tour-elem="right-arrow"] {
order: 2;
margin-left: 0;
}
button[data-tour-elem="left-arrow"] {
order: 1;
margin-right: 10px;
margin-left: auto;
}
button[data-tour-elem="right-arrow"],
button[data-tour-elem="left-arrow"],
.LastStepNextButton {
width: 40px;
height: 40px;
border-radius: 10px;
}
button[data-tour-elem="right-arrow"],
button[data-tour-elem="left-arrow"] {
background-color: #f2f8fd;
}
.LastStepNextButton {
background-color: #ffffff;
cursor: default;
}
button[data-tour-elem="right-arrow"] > img,
button[data-tour-elem="left-arrow"] > img {
margin: 0 auto;
background-color: transparent;
}
div[data-tour-elem="controls"] {
align-items: flex-end;
}
nav > .reactour__dot {
background-color: #e1eefa;
border: none;
}
nav > .reactour__dot:disabled {
cursor: unset;
}
nav > .reactour__dot--is-active {
background-color: #6550f6;
}
.mask {
color: #363d48;
opacity: 0.4;
}
.linkToRepo {
font-weight: bold;
color: #6550f6;
text-decoration: none;
outline: none;
}
.linkToRepo:hover {
text-decoration: underline;
}
@media (max-width: 550px) {
.reactour__helper.helper {
width: 320px;
font-size: 0.9rem;
line-height: 1.1rem;
}
}
@media (max-width: 440px) {
.reactour__helper.helper {
width: 280px;
font-size: 0.8rem;
line-height: 1rem;
}
span[data-tour-elem="badge"] {
line-height: 2.5;
}
div[data-tour-elem="controls"] {
flex-wrap: wrap;
justify-content: center;
}
div[data-tour-elem="controls"] > nav {
width: 100%;
margin-bottom: 10px;
}
button[data-tour-elem="left-arrow"] {
margin-left: unset;
}
}
================================================
FILE: src/components/TourGuide/styled.ts
================================================
import styled from 'styled-components';
import { MAIN1_COLOR, WHITE_COLOR } from 'appConstants/colors';
import { TextSemiBold, GeneralAdaptiveFont, GeneralButtonPadding } from 'typography';
export const LinkButton = styled.a`
${TextSemiBold};
margin-right: 0;
border-radius: 20px;
border: none;
text-decoration: none;
outline: none;
cursor: pointer;
background-color: ${MAIN1_COLOR};
color: ${WHITE_COLOR};
${GeneralAdaptiveFont};
${GeneralButtonPadding}
`;
================================================
FILE: src/components/TourGuide/tourConfig.tsx
================================================
import { Button, InvertedButton, ButtonsBlock } from 'typography';
import { setIsTourOpen } from 'modules/LoginPage/loginPageReducer';
import { LINK_TO_REPO } from 'appConstants';
import { LinkButton } from './styled';
import { Dispatch } from 'redux';
import { History, LocationState } from 'history';
export const tourConfig = (
history: History,
dispatch: Dispatch,
t: (text: string) => string
) => [
{
content: ({ goTo }: { goTo: (step: number) => void }) => (
{t('Welcome to RSS Teams')}
goTo(9)}>{t('Skip')}
),
action: () => history.push('/'),
},
{
selector: '.secondStep',
content: t('You can create new team'),
action: () => history.push('/'),
},
{
selector: '.thirdStep',
content: t('Or join existing team'),
action: () => history.push('/'),
},
{
selector: '.fourthStep',
content: t('You can always leave the course'),
action: () => history.push('/'),
},
{
content: t('On Teams page you can see other teams'),
action: () => history.push('/'),
},
{
content: t('On Dashboard page'),
action: () => history.push('/students'),
},
{
selector: '.seventhStep',
content: t('With filter usage'),
position: 'bottom',
action: () => history.push('/students'),
},
{
selector: '.eighthStep',
content: t('With dropdown you can switch the course'),
position: 'bottom',
action: () => history.push('/students'),
},
{
content: t('On Edit profile page'),
action: () => history.push('/edit-profile'),
},
{
content: t('If you forget something'),
action: () => history.push('/tutorial'),
},
{
content: () => (
{t('Support us')}{' '}
{t('our repo')}
!
{
dispatch(setIsTourOpen(false));
}}
href={LINK_TO_REPO}
target="_blank"
rel="noreferrer"
>
{t('Got it')}
),
action: () => history.push('/'),
},
];
================================================
FILE: src/components/index.ts
================================================
export { App } from './App';
export { Loader } from './Loader';
export { PrivateRoute } from './PrivateRoute';
export { Header } from './Header';
export { InputField } from './InputField';
export { CourseField } from './CourseField';
export { Pagination } from './Pagination';
export { Modal } from './Modal';
export { ModalExpel } from './ModalExpel';
export { ModalJoin } from './ModalJoin';
export { ModalCreateEditTeam } from './ModalCreateEditTeam';
export { ModalCreated } from './ModalCreated';
export { TablePopup } from './TablePopup';
export { FilterForm } from './FilterForm';
export { FilterSelect } from './FilterSelect';
export { Footer } from './Footer';
export { ErrorModal } from './ErrorModal';
export { CommonSelectList } from './CommonSelectList';
export { TourGuide } from './TourGuide';
export { ModalEditCourse } from './ModalEditCourse';
================================================
FILE: src/graphql/mutations/addUserToTeamMutation.ts
================================================
import { gql } from '@apollo/client';
export const ADD_USER_TO_TEAM_MUTATION = gql`
mutation addUserToTeam($data: AddUserToTeamInput!) {
addUserToTeam(data: $data) {
id
firstName
lastName
github
telegram
discord
score
country
city
isAdmin
courses {
id
name
}
teams {
id
password
number
courseId
socialLink
members {
id
firstName
lastName
github
telegram
discord
score
country
city
}
}
}
}
`;
================================================
FILE: src/graphql/mutations/createCourseMutation.ts
================================================
import { gql } from '@apollo/client';
export const CREATE_COURSE_MUTATION = gql`
mutation createCourse($course: CreateCourseInput!) {
createCourse(course: $course) {
id
name
teamSize
isActive
}
}
`;
================================================
FILE: src/graphql/mutations/createTeamMutation.ts
================================================
import { gql } from '@apollo/client';
export const CREATE_TEAM_MUTATION = gql`
mutation createTeam($team: CreateTeamInput!) {
createTeam(team: $team) {
id
password
number
courseId
socialLink
memberIds
members {
id
firstName
lastName
github
telegram
discord
score
country
city
}
}
}
`;
================================================
FILE: src/graphql/mutations/index.ts
================================================
export { UPD_USER_MUTATION } from './updUserMutation';
export { ADD_USER_TO_TEAM_MUTATION } from './addUserToTeamMutation';
export { REMOVE_USER_FROM_TEAM_MUTATION } from './removeUserFromTeamMutation';
export { CREATE_TEAM_MUTATION } from './createTeamMutation';
export { UPDATE_TEAM_MUTATION } from './updateTeamMutation';
export { SORT_STUDENTS_MUTATION } from './sortStudentsMutation';
export { REMOVE_USER_FROM_COURSE_MUTATION } from './removeUserFromCourseMutation';
export { CREATE_COURSE_MUTATION } from './createCourseMutation';
export { UPDATE_COURSE_MUTATION } from './updateCourseMutation';
================================================
FILE: src/graphql/mutations/removeUserFromCourseMutation.ts
================================================
import { gql } from '@apollo/client';
export const REMOVE_USER_FROM_COURSE_MUTATION = gql`
mutation removeUserFromCourse($data: RemoveUserFromCourseInput!) {
removeUserFromCourse(data: $data) {
id
firstName
lastName
github
telegram
discord
score
country
city
isAdmin
courses {
id
name
}
teams {
id
password
number
courseId
socialLink
members {
id
firstName
lastName
github
telegram
discord
score
country
city
}
}
}
}
`;
================================================
FILE: src/graphql/mutations/removeUserFromTeamMutation.ts
================================================
import { gql } from '@apollo/client';
export const REMOVE_USER_FROM_TEAM_MUTATION = gql`
mutation removeUserFromTeam($data: RemoveUserFromTeamInput!) {
removeUserFromTeam(data: $data) {
id
firstName
lastName
github
telegram
discord
score
country
city
avatar
isAdmin
courses {
id
name
}
email
courseIds
teamIds
teams {
id
password
number
courseId
socialLink
memberIds
members {
id
firstName
lastName
github
telegram
discord
score
country
city
}
}
}
}
`;
================================================
FILE: src/graphql/mutations/sortStudentsMutation.ts
================================================
import { gql } from '@apollo/client';
export const SORT_STUDENTS_MUTATION = gql`
mutation sortStudents($courseId: String!) {
sortStudents(courseId: $courseId)
}
`;
================================================
FILE: src/graphql/mutations/updUserMutation.ts
================================================
import { gql } from '@apollo/client';
export const UPD_USER_MUTATION = gql`
mutation updateUser($user: UpdateUserInput!) {
updateUser(user: $user) {
id
firstName
lastName
github
telegram
discord
score
country
city
isAdmin
courses {
id
name
}
teams {
id
password
number
courseId
socialLink
members {
id
firstName
lastName
github
telegram
discord
score
country
city
}
}
}
}
`;
================================================
FILE: src/graphql/mutations/updateCourseMutation.ts
================================================
import { gql } from '@apollo/client';
export const UPDATE_COURSE_MUTATION = gql`
mutation updateCourse($course: UpdateCourseInput!) {
updateCourse(course: $course) {
id
name
teamSize
isActive
}
}
`;
================================================
FILE: src/graphql/mutations/updateTeamMutation.ts
================================================
import { gql } from '@apollo/client';
export const UPDATE_TEAM_MUTATION = gql`
mutation updateTeam($team: UpdateTeamInput!) {
updateTeam(team: $team) {
id
password
number
courseId
socialLink
memberIds
members {
id
firstName
lastName
github
telegram
discord
score
country
city
}
}
}
`;
================================================
FILE: src/graphql/queries/coursesQuery.ts
================================================
import { gql } from '@apollo/client';
export const COURSES_QUERY = gql`
query getCourses {
courses {
id
name
teamSize
isActive
}
}
`;
================================================
FILE: src/graphql/queries/index.ts
================================================
export { TEAMS_QUERY } from './teamsQuery';
export { USERS_QUERY } from './usersQuery';
// export { USER_QUERY } from './userQuery';
export { WHOAMI_QUERY } from './whoAmIQuery';
export { COURSES_QUERY } from './coursesQuery';
================================================
FILE: src/graphql/queries/teamsQuery.ts
================================================
import { gql } from '@apollo/client';
export const TEAMS_QUERY = gql`
query getTeams($courseId: String!, $pagination: PaginationInput!) {
teams(courseId: $courseId, pagination: $pagination) {
count
results {
id
number
courseId
socialLink
members {
id
firstName
lastName
github
telegram
discord
score
country
city
}
}
}
}
`;
================================================
FILE: src/graphql/queries/usersQuery.ts
================================================
import { gql } from '@apollo/client';
export const USERS_QUERY = gql`
query getUsers($courseId: String!, $pagination: PaginationInput!, $filter: UserFilterInput) {
users(courseId: $courseId, pagination: $pagination, filter: $filter) {
count
results {
id
firstName
lastName
github
telegram
discord
score
country
city
isAdmin
courses {
id
name
}
teams {
id
number
courseId
}
}
}
}
`;
================================================
FILE: src/graphql/queries/whoAmIQuery.ts
================================================
import { gql } from '@apollo/client';
export const WHOAMI_QUERY = gql`
query getWhoAMi {
whoAmI {
id
firstName
lastName
github
telegram
discord
score
country
city
isAdmin
courses {
id
name
}
teams {
id
password
number
courseId
socialLink
members {
id
firstName
lastName
github
telegram
discord
score
country
city
}
}
}
}
`;
================================================
FILE: src/hooks/graphql/index.ts
================================================
export { useTeamsQuery } from './queries/useTeamsQuery';
export { useUsersQuery } from './queries/useUsersQuery';
export { useWhoAmIQuery } from './queries/useWhoAmIQuery';
export { useCoursesQuery } from './queries/useCoursesQuery';
export { useUpdUserMutation } from './mutations/useUpdUserMutation';
export { useRemoveUserFromTeamMutation } from './mutations/useRemoveUserFromTeamMutation';
export { useExpelUserFromTeamMutation } from './mutations/useExpelUserFromTeamMutation';
export { useCreateTeamMutation } from './mutations/useCreateTeamMutation';
export { useAddUserToTeamMutation } from './mutations/useAddUserToTeamMutation';
export { useUpdateTeamMutation } from './mutations/useUpdateTeamMutation';
export { useSortStudentsMutation } from './mutations/useSortStudentsMutation';
export { useRemoveUserFromCourseMutation } from './mutations/useRemoveUserFromCourseMutation';
export { useCreateCourseMutation } from './mutations/useCreateCourseMutation';
export { useUpdateCourseMutation } from './mutations/useUpdateCourseMutation';
================================================
FILE: src/hooks/graphql/mutations/useAddUserToTeamMutation.ts
================================================
import { useMutation } from '@apollo/client';
import { ADD_USER_TO_TEAM_MUTATION } from 'graphql/mutations';
import { WHOAMI_QUERY } from 'graphql/queries';
import { AddUserToTeamInput } from 'types';
type Props = {
data: AddUserToTeamInput;
};
export const useAddUserToTeamMutation = ({ data }: Props) => {
const [addUserToTeam, { loading, error }] = useMutation(ADD_USER_TO_TEAM_MUTATION, {
variables: {
data,
},
update(cache, { data: { addUserToTeam } }) {
cache.writeQuery({
query: WHOAMI_QUERY,
data: {
addUserToTeam,
},
});
},
});
return {
addUserToTeam,
loadingM: loading,
errorM: error,
};
};
================================================
FILE: src/hooks/graphql/mutations/useCreateCourseMutation.ts
================================================
import { useMutation } from '@apollo/client';
import { Course, CreateCourseInput } from 'types';
import { COURSES_QUERY } from 'graphql/queries';
import { CREATE_COURSE_MUTATION } from 'graphql/mutations';
type Props = {
course: CreateCourseInput;
};
export const useCreateCourseMutation = ({ course }: Props) => {
const [createCourse, { loading, error }] = useMutation(CREATE_COURSE_MUTATION, {
variables: {
course,
},
update(cache, { data: { createCourse } }) {
const data: { courses: Course[] } | null = cache.readQuery({
query: COURSES_QUERY,
});
const updatedResults = data?.courses?.length
? [createCourse, ...data?.courses]
: [createCourse];
cache.writeQuery({
query: COURSES_QUERY,
data: {
courses: updatedResults,
},
});
},
});
return {
createCourse,
loadingM: loading,
errorM: error,
};
};
================================================
FILE: src/hooks/graphql/mutations/useCreateTeamMutation.ts
================================================
import { useMutation } from '@apollo/client';
import { CreateTeamInput, TeamList, User } from 'types';
import { TEAMS_QUERY, WHOAMI_QUERY } from 'graphql/queries';
import { CREATE_TEAM_MUTATION } from 'graphql/mutations';
import { TEAMS_PER_PAGE } from 'appConstants';
type Props = {
team: CreateTeamInput;
};
export const useCreateTeamMutation = ({ team }: Props) => {
const { courseId, ownerId, socialLink, page } = team;
const dataForMutation = { courseId, ownerId, socialLink };
const [createTeam, { loading, error }] = useMutation(CREATE_TEAM_MUTATION, {
variables: {
team: dataForMutation,
},
update(cache, { data: { createTeam } }) {
const data: { teams: TeamList } | null = cache.readQuery({
query: TEAMS_QUERY,
variables: {
courseId: courseId,
pagination: { skip: page * TEAMS_PER_PAGE, take: TEAMS_PER_PAGE },
},
});
const userData: { whoAmI: User } | null = cache.readQuery({
query: WHOAMI_QUERY,
});
const updatedResults = data?.teams?.results.length
? [...data?.teams?.results, createTeam]
: [createTeam];
const updatedUser = {
...userData?.whoAmI,
teams: userData?.whoAmI?.teams.length
? [...userData?.whoAmI?.teams, createTeam]
: [createTeam],
};
cache.writeQuery({
query: WHOAMI_QUERY,
data: {
whoAmI: updatedUser,
},
});
cache.writeQuery({
query: TEAMS_QUERY,
data: {
teams: {
count: updatedResults.length,
results: updatedResults,
},
},
variables: {
courseId: courseId,
pagination: { skip: page * TEAMS_PER_PAGE, take: TEAMS_PER_PAGE },
},
});
},
});
return {
createTeam,
loadingM: loading,
errorM: error,
};
};
================================================
FILE: src/hooks/graphql/mutations/useExpelUserFromTeamMutation.ts
================================================
import { useMutation } from '@apollo/client';
import { RemoveUserFromTeamInput, Team, TeamList, User } from 'types';
import { TEAMS_QUERY, WHOAMI_QUERY } from 'graphql/queries';
import { REMOVE_USER_FROM_TEAM_MUTATION } from 'graphql/mutations';
import { TEAMS_PER_PAGE } from 'appConstants';
type Props = {
data: RemoveUserFromTeamInput;
};
export const useExpelUserFromTeamMutation = ({ data }: Props) => {
const { teamId, page, userId, courseId } = data;
const dataForMutation = { userId, teamId };
const [expelUserFromTeam, { loading, error }] = useMutation(REMOVE_USER_FROM_TEAM_MUTATION, {
variables: {
data: dataForMutation,
},
update(cache, { data: {} }) {
const data: { teams: TeamList } | null = cache.readQuery({
query: TEAMS_QUERY,
variables: {
courseId: courseId,
pagination: { skip: page * TEAMS_PER_PAGE, take: TEAMS_PER_PAGE },
},
});
const userData: { whoAmI: User } | null = cache.readQuery({
query: WHOAMI_QUERY,
});
const updatedRemovedResults = data?.teams.results.map((team: Team) => {
if (team.id === teamId) {
return {
...team,
members: team.members.filter((member: User) => member.id !== userId),
};
}
return team;
});
const updatedTeams = (userData?.whoAmI.teams as Team[]).map((team: Team) => {
if (team.id === teamId) {
return {
...team,
members: team.members.filter((member: User) => member.id !== userId),
};
}
return team;
});
cache.writeQuery({
query: WHOAMI_QUERY,
data: {
...userData?.whoAmI,
teams: updatedTeams,
},
});
cache.writeQuery({
query: TEAMS_QUERY,
data: {
teams: {
count: data?.teams?.count,
results: updatedRemovedResults,
},
},
variables: {
courseId: courseId,
pagination: { skip: page * TEAMS_PER_PAGE, take: TEAMS_PER_PAGE },
},
});
},
});
return {
expelUserFromTeam,
loadingM: loading,
errorM: error,
};
};
================================================
FILE: src/hooks/graphql/mutations/useRemoveUserFromCourseMutation.ts
================================================
import { useMutation } from '@apollo/client';
import { CURRENT_COURSE, TEAMS_PER_PAGE } from 'appConstants';
import { REMOVE_USER_FROM_COURSE_MUTATION } from 'graphql/mutations';
import { TEAMS_QUERY, WHOAMI_QUERY } from 'graphql/queries';
import { setCourse } from 'modules/LoginPage/loginPageMiddleware';
import { setCommonError, setCurrCourse } from 'modules/LoginPage/loginPageReducer';
import { setUserData } from 'modules/StudentsTable/studentsTableReducer';
import { useDispatch } from 'react-redux';
import { RemoveUserFromCourseInput, Team, TeamList, User } from 'types';
type Props = {
data: RemoveUserFromCourseInput;
};
export const useRemoveUserFromCourseMutation = ({
data: { page, courseId, userId, teamId },
}: Props) => {
const dataForMutation = { courseId, userId, teamId };
const dispatch = useDispatch();
const [removeUserFromCourse, { loading, error }] = useMutation(REMOVE_USER_FROM_COURSE_MUTATION, {
variables: {
data: dataForMutation,
},
update(cache, { data: { removeUserFromCourse } }) {
const data: { teams: TeamList } | null = cache.readQuery({
query: TEAMS_QUERY,
variables: {
courseId,
pagination: { skip: page * TEAMS_PER_PAGE, take: TEAMS_PER_PAGE },
},
});
const updatedRemovedResults = data?.teams.results
.map((team: Team) => {
if (team.id === teamId) {
if (team.members.length === 1) {
return null;
}
return {
...team,
members: team.members.filter((member: User) => member.id !== userId),
};
}
return team;
})
.filter((team: Team | null) => !!team);
cache.writeQuery({
query: WHOAMI_QUERY,
data: {
removeUserFromCourse,
},
});
cache.writeQuery({
query: TEAMS_QUERY,
data: {
teams: {
count: data?.teams?.count,
results: updatedRemovedResults,
},
},
variables: {
courseId: courseId,
pagination: { skip: page * TEAMS_PER_PAGE, take: TEAMS_PER_PAGE },
},
});
},
onCompleted({ removeUserFromCourse }) {
if (!!removeUserFromCourse.courses[0]) {
dispatch(setCourse(removeUserFromCourse.courses[0]));
} else {
dispatch(setCurrCourse({ name: '', id: '' }));
localStorage.removeItem(CURRENT_COURSE);
}
dispatch(setUserData(removeUserFromCourse));
},
onError() {
dispatch(setCommonError(true));
},
});
return {
removeUserFromCourse,
loadingM: loading,
errorM: error,
};
};
================================================
FILE: src/hooks/graphql/mutations/useRemoveUserFromTeamMutation.ts
================================================
import { useMutation } from '@apollo/client';
import { RemoveUserFromTeamInput, Team, TeamList, User } from 'types';
import { TEAMS_QUERY, WHOAMI_QUERY } from 'graphql/queries';
import { REMOVE_USER_FROM_TEAM_MUTATION } from 'graphql/mutations';
import { TEAMS_PER_PAGE } from 'appConstants';
type Props = {
data: RemoveUserFromTeamInput;
};
export const useRemoveUserFromTeamMutation = ({ data }: Props) => {
const { teamId, page, userId, courseId } = data;
const dataForMutation = { userId, teamId };
const [removeUserFromTeam, { loading, error }] = useMutation(REMOVE_USER_FROM_TEAM_MUTATION, {
variables: {
data: dataForMutation,
},
update(cache, { data: { removeUserFromTeam } }) {
const data: { teams: TeamList } | null = cache.readQuery({
query: TEAMS_QUERY,
variables: {
courseId: courseId,
pagination: { skip: page * TEAMS_PER_PAGE, take: TEAMS_PER_PAGE },
},
});
const updatedRemovedResults = data?.teams.results
.map((team: Team) => {
if (team.id === teamId) {
return {
...team,
members: team.members.filter((member: User) => member.id !== userId),
};
}
return team;
})
.filter((team: Team | undefined) => !!team?.members.length);
cache.writeQuery({
query: WHOAMI_QUERY,
data: {
removeUserFromTeam,
},
});
cache.writeQuery({
query: TEAMS_QUERY,
data: {
teams: {
count: updatedRemovedResults?.length,
results: updatedRemovedResults,
},
},
variables: {
courseId: courseId,
pagination: { skip: page * TEAMS_PER_PAGE, take: TEAMS_PER_PAGE },
},
});
},
});
return {
removeUserFromTeam,
loadingM: loading,
errorM: error,
};
};
================================================
FILE: src/hooks/graphql/mutations/useSortStudentsMutation.ts
================================================
import { useMutation } from '@apollo/client';
import { SORT_STUDENTS_MUTATION } from 'graphql/mutations';
type Props = {
courseId: string;
};
export const useSortStudentsMutation = ({ courseId }: Props) => {
const [sortStudents, { loading, error }] = useMutation(SORT_STUDENTS_MUTATION, {
variables: {
courseId,
},
});
return {
sortStudents,
loadingM: loading,
errorM: error,
};
};
================================================
FILE: src/hooks/graphql/mutations/useUpdUserMutation.ts
================================================
import { useMutation } from '@apollo/client';
import { UPD_USER_MUTATION } from 'graphql/mutations';
import { WHOAMI_QUERY } from 'graphql/queries';
import { UpdateUserInput } from 'types';
type Props = {
user: UpdateUserInput;
};
export const useUpdUserMutation = ({ user }: Props) => {
const formattedUser = { ...user, score: Number(user.score) };
const [updateUser, { loading, error }] = useMutation(UPD_USER_MUTATION, {
variables: {
user: formattedUser,
},
update(cache, { data: { updateUser } }) {
cache.writeQuery({
query: WHOAMI_QUERY,
data: {
updateUser,
},
});
},
});
return {
updateUser,
loadingM: loading,
errorM: error,
};
};
================================================
FILE: src/hooks/graphql/mutations/useUpdateCourseMutation.ts
================================================
import { useMutation } from '@apollo/client';
import { UpdateCourseInput, Course } from 'types';
import { COURSES_QUERY } from 'graphql/queries';
import { UPDATE_COURSE_MUTATION } from 'graphql/mutations';
type Props = {
course: UpdateCourseInput;
};
export const useUpdateCourseMutation = ({ course }: Props) => {
const { id } = course;
const [updateCourse, { loading, error }] = useMutation(UPDATE_COURSE_MUTATION, {
variables: {
course,
},
update(cache, { data: { updateCourse } }) {
const data: { courses: Course[] } | null = cache.readQuery({
query: COURSES_QUERY,
});
const updatedResults = data?.courses?.map((course: Course) => {
if (course.id === id) {
return updateCourse;
}
return course;
});
cache.writeQuery({
query: COURSES_QUERY,
data: {
courses: updatedResults,
},
});
},
});
return {
updateCourse,
loadingM: loading,
errorM: error,
};
};
================================================
FILE: src/hooks/graphql/mutations/useUpdateTeamMutation.ts
================================================
import { useMutation } from '@apollo/client';
import { UpdateTeamInput, User, Team } from 'types';
import { WHOAMI_QUERY } from 'graphql/queries';
import { UPDATE_TEAM_MUTATION } from 'graphql/mutations';
type Props = {
team: UpdateTeamInput;
};
export const useUpdateTeamMutation = ({ team }: Props) => {
const { id } = team;
const [updateTeam, { loading, error }] = useMutation(UPDATE_TEAM_MUTATION, {
variables: {
team,
},
update(cache, { data: { updateTeam } }) {
const userData: { whoAmI: User } | null = cache.readQuery({
query: WHOAMI_QUERY,
});
const updatedUserTeam = (userData?.whoAmI.teams as Team[]).map((team: Team) => {
if (team.id === id) {
return updateTeam;
}
return team;
});
cache.writeQuery({
query: WHOAMI_QUERY,
data: {
whoAmI: {
...userData?.whoAmI,
teams: updatedUserTeam,
},
},
});
},
});
return {
updateTeam,
loadingM: loading,
errorM: error,
};
};
================================================
FILE: src/hooks/graphql/queries/useCoursesQuery.ts
================================================
import { useQuery } from '@apollo/client';
import { COURSES_QUERY } from 'graphql/queries';
export const useCoursesQuery = () => {
const { data, loading, error } = useQuery(COURSES_QUERY);
const isLoaded = !loading && data?.courses;
return {
loading: !isLoaded,
error,
courses: data?.courses,
};
};
================================================
FILE: src/hooks/graphql/queries/useTeamsQuery.ts
================================================
import { useQuery } from '@apollo/client';
import { TEAMS_PER_PAGE } from 'appConstants';
import { TEAMS_QUERY } from 'graphql/queries';
type Props = {
reactCourseId: string;
skip?: boolean;
page?: number;
};
export const useTeamsQuery = ({ reactCourseId, skip = false, page = 0 }: Props) => {
const { data, loading, error } = useQuery(TEAMS_QUERY, {
skip,
variables: {
courseId: reactCourseId,
pagination: {
skip: page * TEAMS_PER_PAGE,
take: TEAMS_PER_PAGE,
},
},
});
const isLoaded = !loading && !!data;
return {
loadingT: !isLoaded,
errorT: error,
teams: data?.teams,
};
};
================================================
FILE: src/hooks/graphql/queries/useUsersQuery.ts
================================================
import { useQuery } from '@apollo/client';
import { USERS_PER_PAGE } from 'appConstants';
import { USERS_QUERY } from 'graphql/queries';
import { UserFilterInput } from 'types';
type Props = {
reactCourseId: string;
skip?: boolean;
page?: number;
filter?: UserFilterInput;
};
export const useUsersQuery = ({ reactCourseId, skip = false, page = 0, filter }: Props) => {
const { data, loading, error } = useQuery(USERS_QUERY, {
skip,
variables: {
filter,
courseId: reactCourseId,
pagination: {
skip: page * USERS_PER_PAGE,
take: USERS_PER_PAGE,
},
},
});
const isLoaded = !loading && !!data;
return {
loadingU: !isLoaded,
errorU: error,
users: data?.users,
};
};
================================================
FILE: src/hooks/graphql/queries/useWhoAmIQuery.ts
================================================
import { useQuery } from '@apollo/client';
import { WHOAMI_QUERY } from 'graphql/queries';
type Props = {
skip: boolean;
};
export const useWhoAmIQuery = ({ skip = false }: Props) => {
const {
data,
loading: loadingW,
error,
} = useQuery(WHOAMI_QUERY, {
skip,
});
return {
loadingW,
errorW: error,
whoAmI: data?.whoAmI,
};
};
================================================
FILE: src/index.tsx
================================================
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router } from 'react-router-dom';
import { AppState } from 'store';
import { ApolloProvider, ApolloClient, createHttpLink, InMemoryCache, from } from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
import { App } from './components';
import { AUTH_TOKEN, BACKEND_LINK, UNAUTHORIZED_ERROR_MESSAGE } from 'appConstants';
import reportWebVitals from './reportWebVitals';
import 'typography/normalize.css';
import 'typography/fonts.css';
import 'typography/common.css';
import './translation/resources';
import ErrorBoundary from 'components/ErrorBoundary';
import { onError } from '@apollo/client/link/error';
const httpLink = createHttpLink({
uri: BACKEND_LINK,
});
const unauthorizedLink = onError(({ graphQLErrors }) => {
const isUserUnauthorized = !!graphQLErrors?.find(
({ message }) => message === UNAUTHORIZED_ERROR_MESSAGE
);
if (isUserUnauthorized) {
location.reload();
sessionStorage.removeItem(AUTH_TOKEN);
}
});
const authLink = setContext((_, { headers }) => {
const token = sessionStorage.getItem(AUTH_TOKEN);
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : '',
},
};
});
const client = new ApolloClient({
link: from([unauthorizedLink, authLink, httpLink]),
cache: new InMemoryCache({
typePolicies: {
User: {
fields: {
teams: {
merge(_, incoming) {
return incoming;
},
},
courses: {
merge(_, incoming) {
return incoming;
},
},
},
},
Team: {
fields: {
members: {
merge(_, incoming) {
return incoming;
},
},
},
},
Query: {
fields: {
teams: {
merge(_, incoming) {
return incoming;
},
},
},
},
},
}),
connectToDevTools: true,
});
ReactDOM.render(
,
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
================================================
FILE: src/modules/AdminPage/components/ContentWrapper/components/AddCourseBlock/components/InputsBlock/index.tsx
================================================
import React, { FC } from 'react';
import { useTranslation } from 'react-i18next';
import { ModalInput, Label } from 'typography';
import { ValidationAlert } from 'components/InputField/styled';
import { InputWrapper, FieldWrapper } from './styled';
interface InputBlockProps {
inputLabel: string;
value: string;
placeholder: string;
onChangeHandler: (e: React.ChangeEvent) => void;
isFieldValid: boolean;
errorMessage: string;
isModal?: boolean;
}
export const InputBlock: FC = ({
inputLabel,
value,
placeholder,
onChangeHandler,
isFieldValid,
errorMessage,
isModal,
}) => {
const { t } = useTranslation();
return (
{!isFieldValid && {t(errorMessage)}}
);
};
================================================
FILE: src/modules/AdminPage/components/ContentWrapper/components/AddCourseBlock/components/InputsBlock/styled.ts
================================================
import styled from 'styled-components';
export const InputWrapper = styled.div<{ isModal?: boolean }>`
display: flex;
${({ isModal }) => isModal && 'flex-direction: column;'}
justify-content: space-between;
align-items: center;
width: 100%;
height: fit-content;
${({ isModal }) => isModal && 'margin-bottom: 40px;'}
gap: ${({ isModal }) => (isModal ? '5px' : '20px')};
@media (max-width: 580px) {
${({ isModal }) => isModal && 'margin-bottom: 30px;'}
label {
${({ isModal }) => !isModal && 'display: none;'}
}
}
`;
export const FieldWrapper = styled.div`
position: relative;
display: flex;
flex-direction: column;
& > div:nth-child(2) {
position: absolute;
top: 100%;
}
`;
================================================
FILE: src/modules/AdminPage/components/ContentWrapper/components/AddCourseBlock/index.tsx
================================================
import React, { FC, useCallback, useState } from 'react';
import { MAIN2_COLOR, WHITE_COLOR } from 'appConstants/colors';
import { useTranslation } from 'react-i18next';
import { TeamButton } from 'typography';
import { InputBlock } from './components/InputsBlock';
import { AddCourseBlockWrapper, InputsBlockWrapper } from './styled';
import { isFieldValid } from 'utils/isFieldValid';
import { COURSE_NAME_VALIDATION, TEAM_SIZE_VALIDATION } from 'appConstants';
import { useCreateCourseMutation } from 'hooks/graphql';
import { ErrorModal, Modal } from 'components';
import { activeModalCreatedCourse } from 'modules/TeamsList/teamsListReducer';
import { useDispatch, useSelector } from 'react-redux';
import { selectIsActiveModalCreatedCourse } from 'modules/TeamsList/selectors';
export const onChangeField =
(
validateRules: any,
setInputValid: (isValid: boolean) => void,
setErrorMessage: (errorMessage: string) => void,
setInputValue: (value: string) => void,
isValueUniq?: (value: string) => boolean
) =>
(e: React.ChangeEvent) => {
isFieldValid(
e.target.value.trim(),
validateRules,
!!validateRules,
setInputValid,
setErrorMessage,
isValueUniq
);
setInputValue(e.target.value);
};
export const AddCourseBlock: FC<{ checkIsCourseNameUniq: (courseName: string) => boolean }> = ({
checkIsCourseNameUniq,
}) => {
const dispatch = useDispatch();
const { t } = useTranslation();
const isActiveModalCreatedCourse = useSelector(selectIsActiveModalCreatedCourse);
const [isCourseNameFieldValid, setIsCourseNameFieldValid] = useState(true);
const [isTeamSizeFieldValid, setIsTeamSizeFieldValid] = useState(true);
const [courseName, setCourseName] = useState('');
const [teamSize, setTeamSize] = useState('');
const [courseNameErrorMessage, setCourseNameErrorMessage] = useState('');
const [teamSizeErrorMessage, setTeamSizeErrorMessage] = useState('');
const { createCourse, errorM: errorCreateCourse } = useCreateCourseMutation({
course: {
name: courseName,
teamSize: +teamSize,
isActive: true,
},
});
const onAddNewCourse = useCallback(() => {
if (isCourseNameFieldValid && isTeamSizeFieldValid) {
if (courseName && teamSize) {
createCourse().then(() => {
dispatch(activeModalCreatedCourse(true));
});
} else {
if (!courseName) {
setIsCourseNameFieldValid(false);
setCourseNameErrorMessage(COURSE_NAME_VALIDATION.minLength.message);
}
if (!teamSize) {
setIsTeamSizeFieldValid(false);
setTeamSizeErrorMessage(TEAM_SIZE_VALIDATION.minLength.message);
}
}
}
}, [isCourseNameFieldValid, isTeamSizeFieldValid, courseName, teamSize, createCourse, dispatch]);
if (errorCreateCourse) return ;
return (
<>
{t('Add new course')}
{
setCourseName('');
setTeamSize('');
dispatch(activeModalCreatedCourse(false));
}}
cancelText="Got it!"
open={isActiveModalCreatedCourse}
hideOnOutsideClick
hideOnEsc
>
>
);
};
================================================
FILE: src/modules/AdminPage/components/ContentWrapper/components/AddCourseBlock/styled.ts
================================================
import styled from 'styled-components';
export const AddCourseBlockWrapper = styled.div`
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 250px;
gap: 4%;
@media (max-width: 850px) {
flex-direction: column;
}
`;
export const InputsBlockWrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: fit-content;
height: fit-content;
gap: 20px;
@media (max-width: 850px) {
margin-bottom: 30px;
}
`;
================================================
FILE: src/modules/AdminPage/components/ContentWrapper/components/CoursesList/components/Course/index.tsx
================================================
import React, { FC } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { CourseButton } from 'typography';
import { CourseWrapper } from './styled';
import { activeModalEditCourse, activeModalSortStudents } from 'modules/TeamsList/teamsListReducer';
import { Course } from 'types';
interface ICourseItem {
index: number;
id: string;
name: string;
teamSize: number | null;
isActive: boolean;
setCourseInfoToSort: (course: Partial) => void;
setCourseEditMeta: (course: Course | null) => void;
}
export const CourseItem: FC = ({
index,
id,
name,
teamSize,
isActive,
setCourseInfoToSort,
setCourseEditMeta,
}) => {
const dispatch = useDispatch();
const { t } = useTranslation();
const courseOrder = index + 1;
const courseState = isActive ? 'Active(status)' : 'Terminate(status)';
const onClickSortStudents = () => {
setCourseInfoToSort({ id, name });
dispatch(activeModalSortStudents(true));
};
const onClickEditCourse = () => {
setCourseEditMeta({ id, name, isActive, teamSize });
dispatch(activeModalEditCourse(true));
};
return (
{courseOrder}
{name}
{teamSize ?? t('Unset')}
{t(courseState)}
{t('Edit course')}
{t('Sort students')}
);
};
================================================
FILE: src/modules/AdminPage/components/ContentWrapper/components/CoursesList/components/Course/styled.ts
================================================
import styled from 'styled-components';
import { BG_COLOR } from 'appConstants/colors';
import { GeneralAdaptiveFont } from 'typography';
export const CourseWrapper = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
width: 90%;
background-color: ${BG_COLOR};
padding: 20px;
border-radius: 20px;
& > div:nth-child(1) {
font-weight: 600;
}
& > div {
${GeneralAdaptiveFont};
}
@media (max-width: 1100px) {
flex-direction: column;
width: 40%;
gap: 10px;
& > div:nth-child(1) {
text-align: left;
}
button,
div {
width: 90%;
text-align: center;
}
}
@media (max-width: 768px) {
width: 80%;
}
@media (max-width: 450px) {
gap: 5px;
}
`;
================================================
FILE: src/modules/AdminPage/components/ContentWrapper/components/CoursesList/index.tsx
================================================
import React, { FC } from 'react';
import { CoursesListWrapper } from './styled';
import { CourseItem } from './components/Course';
import { Course } from 'types';
import { ErrorModal, ModalEditCourse, ModalExpel } from 'components';
import { useDispatch, useSelector } from 'react-redux';
import {
selectIsActiveModalEditCourse,
selectIsActiveModalSortStudents,
} from 'modules/TeamsList/selectors';
import { activeModalSortStudents } from 'modules/TeamsList/teamsListReducer';
import { useSortStudentsMutation } from 'hooks/graphql';
import { useState } from 'react';
export const CoursesList: FC<{
courses: Course[];
checkIsCourseNameUniq: (courseName: string) => boolean;
}> = ({ courses, checkIsCourseNameUniq }) => {
const [courseInfoToSort, setCourseInfoToSort] = useState | null>(null);
const [courseEditMeta, setCourseEditMeta] = useState(null);
const dispatch = useDispatch();
const isActiveModalSortStudents = useSelector(selectIsActiveModalSortStudents);
const isActiveModalEditCourse = useSelector(selectIsActiveModalEditCourse);
const { sortStudents, errorM: errorSortStudents } = useSortStudentsMutation({
courseId: courseInfoToSort?.id || '',
});
const onSubmitSortStudents = () => {
sortStudents();
};
if (errorSortStudents) return ;
return (
<>
{courses.map((course: Course, index: number) => (
))}
dispatch(activeModalSortStudents(false))}
okText="Yes"
cancelText="No"
/>
{!!courseEditMeta && (
)}
>
);
};
================================================
FILE: src/modules/AdminPage/components/ContentWrapper/components/CoursesList/styled.ts
================================================
import styled from 'styled-components';
export const CoursesListWrapper = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
gap: 20px;
@media (max-width: 1100px) and (min-width: 768px) {
flex-wrap: wrap;
flex-direction: row;
align-items: stretch;
}
`;
================================================
FILE: src/modules/AdminPage/components/ContentWrapper/components/ShowCourseSelect/index.tsx
================================================
import React, { FC, useState } from 'react';
import { SHOW_COURSES_OPTIONS } from 'appConstants';
import { CommonSelectList } from 'components';
interface ShowCourseSelectProps {
currentOption: string;
setCurrentOption: (newOption: string) => void;
}
export const ShowCourseSelect: FC = ({
currentOption,
setCurrentOption,
}) => {
const [isSelectOpen, setIsSelectOpen] = useState(false);
const onOptionChange = (item: { id: string; name: string }) => {
setCurrentOption(item.name);
};
const options: { id: string; name: string }[] = SHOW_COURSES_OPTIONS.filter(
(option) => option.name !== currentOption
);
return (
);
};
================================================
FILE: src/modules/AdminPage/components/ContentWrapper/index.tsx
================================================
import React, { FC, useState, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { AdminPageContentWrapper, ListTitle, CourseListSettings } from './styled';
import { HeaderDecor } from 'modules/TeamsList/components/Teams/components/MyTeam/styled';
import { AddCourseBlock } from './components/AddCourseBlock';
import { CoursesList } from './components/CoursesList';
import { useCoursesQuery } from 'hooks/graphql';
import { ErrorModal, Loader } from 'components';
import { Course } from 'types';
import { ShowCourseSelect } from './components/ShowCourseSelect';
import { ModalInput } from 'typography';
import { SHOW_COURSES_OPTIONS } from 'appConstants';
const filterCourseList = (courses: Course[], filter: string, searchValue?: string) => {
switch (filter) {
case 'Active':
return courses.filter(
({ isActive, name }) => isActive && (searchValue ? name.includes(searchValue) : true)
);
case 'Terminated':
return courses.filter(
({ isActive, name }) => !isActive && (searchValue ? name.includes(searchValue) : true)
);
case 'All':
default:
return searchValue ? courses.filter(({ name }) => name.includes(searchValue)) : courses;
}
};
export const AdminPageWrapper: FC = () => {
const { t } = useTranslation();
const { loading, courses, error } = useCoursesQuery();
const [currentShowCoursesOption, setCurrentShowCoursesOption] = useState(
SHOW_COURSES_OPTIONS[0].name
);
const [searchValue, setSearchValue] = useState('');
const checkIsCourseNameUniq = (courseName: string) => {
return !courses.find(({ name }: Course) => name === courseName);
};
const filteredCourseList = useMemo(
() => filterCourseList(courses, currentShowCoursesOption, searchValue),
[courses, currentShowCoursesOption, searchValue]
);
if (error) return ;
if (loading) return ;
return (
{t('Courses list')}
) =>
setSearchValue(e.target.value.trimLeft())
}
placeholder={t('Search course')}
/>
);
};
================================================
FILE: src/modules/AdminPage/components/ContentWrapper/styled.ts
================================================
import { DARK_TEXT_COLOR, WHITE_COLOR } from 'appConstants/colors';
import styled from 'styled-components';
import { H2AdaptiveFont } from 'typography';
export const AdminPageContentWrapper = styled.div`
position: relative;
z-index: 0;
display: flex;
flex-direction: column;
width: 100%;
max-width: 1320px;
min-height: 75vh;
padding: 20px 0;
background-color: ${WHITE_COLOR};
border-radius: 20px;
`;
export const ListTitle = styled.h4`
${H2AdaptiveFont}
font-size: 25px;
color: ${DARK_TEXT_COLOR};
text-align: center;
`;
export const CourseListSettings = styled.div`
position: relative;
display: flex;
justify-content: flex-end;
padding: 10px 5% 30px;
@media (max-width: 1100px) {
justify-content: flex-start;
padding: 10px 9% 80px;
}
@media (max-width: 768px) {
padding: 10px 10% 80px;
}
`;
================================================
FILE: src/modules/AdminPage/components/index.ts
================================================
export { AdminPageWrapper } from './ContentWrapper';
================================================
FILE: src/modules/AdminPage/index.tsx
================================================
import React, { FC } from 'react';
import { TeamsTitleWrapper } from 'modules/TeamsList/styled';
import { useTranslation } from 'react-i18next';
import { ContentPageWrapper } from 'typography';
import { StudentTableWrapper, TableTitle } from 'modules/StudentsTable/styled';
import { AdminPageWrapper } from './components';
export const AdminPage: FC = () => {
const { t } = useTranslation();
return (
{t('Admin page')}
);
};
================================================
FILE: src/modules/EditProfile/components/UserCourseListItem/index.tsx
================================================
import React, { FC } from 'react';
import { useSelector } from 'react-redux';
import { MinusButton, UserCourseListItemStyled } from './styled';
import { Course, Team } from 'types';
import { ReactComponent as CrossSvgIcon } from 'assets/svg/cross.svg';
import { selectUserData } from 'modules/StudentsTable/selectors';
import { useRemoveUserFromCourseMutation } from 'hooks/graphql';
type TUserCourseListItem = {
isUserRegisteredCourse: boolean;
onSub: (c: IOldCourses) => void;
course: Course;
};
export interface IOldCourses extends Course {
isNew: boolean;
}
export const UserCourseListItem: FC = ({
children,
isUserRegisteredCourse,
onSub,
course,
}) => {
const userData = useSelector(selectUserData);
const { removeUserFromCourse } = useRemoveUserFromCourseMutation({
data: {
courseId: course.id,
userId: userData.id,
teamId: userData.teams.find((team: Team) => team.courseId === course.id)?.id ?? null,
page: 0,
},
});
const onClickHandler = isUserRegisteredCourse
? () => {
onSub({ ...course, isNew: false });
removeUserFromCourse();
}
: () => {
onSub({ ...course, isNew: true });
};
return (
{children}
);
};
================================================
FILE: src/modules/EditProfile/components/UserCourseListItem/styled.ts
================================================
import styled from 'styled-components';
import { BG_COLOR, DARK_TEXT_COLOR } from 'appConstants/colors';
import { PlusButton } from 'components/CourseField/styled';
import { GeneralAdaptiveFont } from 'typography';
export const UserCourseListItemStyled = styled.div`
display: flex;
div {
flex-grow: 1;
padding: 8px 15px;
margin-bottom: 20px;
border-radius: 10px;
background-color: ${BG_COLOR};
color: ${DARK_TEXT_COLOR};
${GeneralAdaptiveFont}
}
`;
export const MinusButton = styled(PlusButton)`
background-image: none;
display: flex;
align-items: center;
justify-content: center;
`;
================================================
FILE: src/modules/EditProfile/formFields.ts
================================================
import { INPUT_VALUES_EDIT_PROFILE } from 'appConstants';
import { Course } from 'types';
import { InputFieldProps } from '../../components/InputField';
export const formFields: InputFieldProps[] = [
{
name: 'firstName',
labelText: 'First Name',
placeholder: 'Enter first name',
register: {
required: 'This is required.',
pattern: {
value: /^[A-Za-z]+$/i,
message: 'This input is letters only.',
},
minLength: {
value: 2,
message: 'Minimal length is 2.',
},
maxLength: {
value: 15,
message: 'This input exceed maxLength.',
},
},
},
{
name: 'lastName',
labelText: 'Last Name',
placeholder: 'Enter last name',
register: {
required: 'This is required.',
pattern: {
value: /^[A-Za-z]+$/i,
message: 'This input is letters only.',
},
minLength: {
value: 2,
message: 'Minimal length is 2.',
},
maxLength: {
value: 20,
message: 'This input exceed maxLength.',
},
},
},
{
name: 'discord',
labelText: 'Discord',
placeholder: 'Enter discord',
register: {
required: 'This is required.',
pattern: {
value: /^[A-Za-z0-9@#-_() ]+$/i,
message: 'This input is letters and digits only.',
},
minLength: {
value: 3,
message: 'Minimal length is 3.',
},
maxLength: {
value: 30,
message: 'This input exceed maxLength.',
},
},
},
{
name: 'telegram',
labelText: 'Telegram',
placeholder: 'Enter telegram',
register: {
required: 'This is required.',
pattern: {
value: /^[A-Za-z0-9-_ ]+$/i,
message: 'This input is letters and digits only.',
},
minLength: {
value: 3,
message: 'Minimal length is 3.',
},
maxLength: {
value: 30,
message: 'This input exceed maxLength.',
},
},
},
{
name: 'city',
labelText: 'City',
placeholder: 'Enter city',
register: {
required: 'This is required.',
pattern: {
value: /^[A-Za-z\- ]+$/i,
message: 'This input is letters only.',
},
minLength: {
value: 2,
message: 'Minimal length is 2.',
},
maxLength: {
value: 30,
message: 'This input exceed maxLength.',
},
},
},
{
name: 'country',
labelText: 'Country',
placeholder: 'Enter country',
register: {
required: 'This is required.',
pattern: {
value: /^[A-Za-z\- ]+$/i,
message: 'This input is letters only.',
},
minLength: {
value: 2,
message: 'Minimal length is 2.',
},
maxLength: {
value: 30,
message: 'This input exceed maxLength.',
},
},
},
{
name: 'score',
labelText: 'Score',
placeholder: 'Enter score',
register: {
required: 'This is required.',
pattern: {
value: /^[1-9]+\d*$/i,
message: 'This input is number only.',
},
minLength: {
value: 1,
message: 'Minimal length is 1.',
},
maxLength: {
value: 5,
message: 'This input exceed maxLength.',
},
},
},
];
export const checkIsCoursesEqual = (newCourses: Course[], userCourses: Course[]) => {
return (
JSON.stringify(newCourses.map((newCourse) => newCourse.name).sort()) ===
JSON.stringify(userCourses.map((userCourse) => userCourse.name).sort())
);
};
export const checkIsFormFieldsEqual = (inputValues: any, userData: any) => {
let isEqual = true;
INPUT_VALUES_EDIT_PROFILE.forEach((item: string) => {
if (inputValues[item] !== userData[item]) {
isEqual = false;
}
});
return isEqual;
};
================================================
FILE: src/modules/EditProfile/index.tsx
================================================
import React, { FC, useEffect, useMemo, useState } from 'react';
import { Redirect, useHistory } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import { FieldError, useForm } from 'react-hook-form';
import { Loader, InputField, CourseField, ErrorModal, ModalExpel } from 'components';
import { useCoursesQuery, useUpdUserMutation } from 'hooks/graphql';
import { Button, AdditionalWrapper } from 'typography';
import { selectUserData } from 'modules/StudentsTable/selectors';
import { selectIsCommonError, selectPathToThePage, selectToken } from 'modules/LoginPage/selectors';
import { Course, UpdateUserInput, User } from 'types';
import { checkIsCoursesEqual, formFields, checkIsFormFieldsEqual } from './formFields';
import {
EditProfileWrapper,
InputsWrapper,
ButtonWrapper,
FormWrapper,
UserCoursesListTitle,
FormTitle,
CoursesWrapper,
CommonWrapper,
} from './styled';
import { BG_COLOR, MAIN1_COLOR } from 'appConstants/colors';
import { INPUT_VALUES_EDIT_PROFILE } from 'appConstants';
import { IOldCourses, UserCourseListItem } from './components/UserCourseListItem';
import { useTranslation } from 'react-i18next';
import { setUserData } from 'modules/StudentsTable/studentsTableReducer';
import { setCourse } from 'modules/LoginPage/loginPageMiddleware';
import { setEditProfileDataChange } from 'modules/LoginPage/loginPageReducer';
import { activeModalLeavePage } from 'modules/TeamsList/teamsListReducer';
import { selectIsActiveModalLeavePage } from 'modules/TeamsList/selectors';
export const EditProfile: FC = () => {
const history = useHistory();
const loginToken = useSelector(selectToken);
const userData = useSelector(selectUserData);
const isCommonError = useSelector(selectIsCommonError);
const { t } = useTranslation();
const isActiveModalLeavePage = useSelector(selectIsActiveModalLeavePage);
const pathToThePage = useSelector(selectPathToThePage);
const oldCourses: IOldCourses[] = userData.courses.map((course: Course) => ({
...course,
isNew: true,
}));
const [userCourses, setUserCourses] = useState(oldCourses);
const { loading, courses, error } = useCoursesQuery();
const defaultData = useMemo(
() => ({
id: userData.id,
firstName: userData.firstName || '',
lastName: userData.lastName || '',
discord: userData.discord || '',
telegram: userData.telegram || '',
city: userData.city || '',
country: userData.country || '',
courseIds: [],
score: userData.score || 9999,
}),
[userData]
);
const [inputValues, setInputValues] = useState(defaultData);
const { updateUser, loadingM, errorM } = useUpdUserMutation({
user: {
...inputValues,
courseIds: userCourses.map(({ id }: Course) => id),
},
});
const [isValidCoursesList, setValidCoursesList] = useState(true);
const dispatch = useDispatch();
const isUserNew = !!userData.courses.length;
const currentCourses = courses?.filter(
({ isActive, name }: Course) =>
isActive && !userCourses.find((uItem: Course) => uItem.name === name)
);
const { register, handleSubmit, errors, reset } = useForm({
defaultValues: inputValues,
mode: 'onChange',
});
const changeInputValue = (e: React.ChangeEvent): void => {
const { name, value } = e.target;
setInputValues({
...inputValues,
[name]: value.trim(),
});
dispatch(
setEditProfileDataChange(
!checkIsFormFieldsEqual(
{
...inputValues,
[name]: value.trim(),
},
userData
)
)
);
};
const onSubmit = () => {
if (userCourses.length) {
updateUser().then(({ data: { updateUser } }) => {
const newCurrentCourse =
updateUser.courses.find(
(course: Course) => course.id === userCourses[userCourses.length - 1].id
) ?? updateUser.courses[0];
dispatch(setCourse(newCurrentCourse));
dispatch(setUserData(updateUser));
history.push('/');
});
} else {
setValidCoursesList(false);
}
dispatch(setEditProfileDataChange(false));
};
const localCourseUpdate = (course: IOldCourses) => {
if (course) {
setUserCourses([...userCourses, course]);
setValidCoursesList(true);
dispatch(setEditProfileDataChange(true));
}
};
const localCourseSub = (course: IOldCourses) => {
if (course) {
const copyCourses: IOldCourses[] = [...userCourses];
const index = copyCourses.findIndex((item: IOldCourses) => {
return item.id === course.id;
});
if (index >= 0) {
copyCourses.splice(index, 1);
}
if (checkIsCoursesEqual(copyCourses, userData.courses)) {
dispatch(setEditProfileDataChange(false));
}
setUserCourses([...copyCourses]);
setValidCoursesList(true);
}
};
const onSubmitLeavePage = () => {
history.push(pathToThePage);
dispatch(setEditProfileDataChange(false));
};
useEffect(() => {
if (userData.id !== '' && !inputValues.id) {
setInputValues(defaultData);
setUserCourses(oldCourses);
reset(defaultData);
dispatch(setEditProfileDataChange(false));
}
}, [reset, inputValues, defaultData, userData, oldCourses, dispatch]);
if (!loginToken) return ;
if (error || errorM || isCommonError) return ;
if (loading || loadingM) return ;
return (
{t('Enter your profile information')}
{formFields.map((item, index) => {
return (
);
})}
{t('Course')}
{userCourses.map((item: IOldCourses) => {
return (
{item.name}
);
})}
{currentCourses.length !== 0 && (
)}
{userCourses.length === 0 && currentCourses.length === 0 && (
No courses available, try again later
)}
{isUserNew && (
)}
dispatch(activeModalLeavePage(false))}
okText="Yes"
cancelText="No"
/>
);
};
================================================
FILE: src/modules/EditProfile/styled.ts
================================================
import styled from 'styled-components';
import { BG_COLOR, DARK_TEXT_COLOR, LIGHT_TEXT_COLOR, WHITE_COLOR } from 'appConstants/colors';
import {
PageTitle,
H1AdaptiveFont,
GeneralAdaptiveFont,
ScrollBar,
MainComponentHeight,
} from 'typography';
export const EditProfileWrapper = styled.form`
background-color: ${WHITE_COLOR};
width: 680px;
padding: 30px;
border-radius: 20px;
margin: 75px 0 15px;
@media screen and (max-width: 768px) {
width: 320px;
padding: 15px 10px;
flex-direction: column;
margin: 50px 0 30px;
}
@media (max-width: 440px) {
width: 280px;
}
`;
export const FormWrapper = styled.div`
position: relative;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
overflow-y: scroll;
${ScrollBar};
${MainComponentHeight};
`;
export const FormTitle = styled(PageTitle)`
margin-top: 0;
margin-bottom: 32px;
${H1AdaptiveFont};
`;
export const InputsWrapper = styled.div`
display: flex;
flex-direction: row;
justify-content: space-between;
flex-wrap: wrap;
margin-bottom: 10px;
@media screen and (max-width: 768px) {
flex-direction: column;
}
`;
export const ButtonWrapper = styled.div`
display: flex;
justify-content: flex-end;
`;
export const CoursesWrapper = styled.div`
max-width: 300px;
width: 100%;
`;
export const UserCoursesListTitle = styled.div`
color: ${LIGHT_TEXT_COLOR};
margin-bottom: 10px;
${GeneralAdaptiveFont}
`;
export const UserCourseListItem = styled.div`
padding: 8px 15px;
margin-bottom: 20px;
border-radius: 10px;
background-color: ${BG_COLOR};
color: ${DARK_TEXT_COLOR};
`;
export const CommonWrapper = styled.div`
position: absolute;
top: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
`;
================================================
FILE: src/modules/LoginPage/components/LoginInfoBlock/index.tsx
================================================
import React, { FC } from 'react';
import {
StyledLoginInfoBlock,
StyledLoginTitle,
StyledLoginRegistrationLink,
StyledLoginTextWrapper,
} from './styled';
import { AUTH_BACKEND_LINK } from 'appConstants';
import { useTranslation } from 'react-i18next';
export const LoginInfoBlock: FC = () => {
const { t } = useTranslation();
return (
{t('Sign in')}
{t('Sign in with Github')}
{t('Don’t have github account?')}
{t('Sign up')}
);
};
================================================
FILE: src/modules/LoginPage/components/LoginInfoBlock/styled.ts
================================================
import styled from 'styled-components';
import { WHITE_COLOR, DARK_TEXT_COLOR, MAIN1_COLOR } from 'appConstants/colors';
import { GeneralAdaptiveFont, GeneralButtonPadding, H1AdaptiveFont } from 'typography';
export const StyledLoginInfoBlock = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-left: 12%;
padding: 20px;
font-size: 1rem;
color: ${DARK_TEXT_COLOR};
background-color: ${WHITE_COLOR};
border-radius: 20px;
gap: 30px;
${GeneralAdaptiveFont};
@media (max-width: 992px) {
margin: 0 auto;
}
@media (max-width: 768px) {
padding: 20px 15px;
gap: 25px;
}
@media (max-width: 550px) {
gap: 20px;
}
@media (max-width: 440px) {
gap: 15px;
}
`;
export const StyledLoginTitle = styled.h2`
margin: 0;
font-weight: 600;
${H1AdaptiveFont};
`;
export const StyledLoginRegistrationLink = styled.a`
display: inline-block;
margin-top: 10px;
text-align: center;
color: ${WHITE_COLOR};
text-decoration: none;
background-color: ${MAIN1_COLOR};
border-radius: 20px;
${GeneralButtonPadding}
`;
export const StyledLoginTextWrapper = styled.div`
display: flex;
font-weight: normal;
line-height: 150%;
gap: 10px;
p {
margin: 0;
}
a {
font-weight: 500;
color: ${MAIN1_COLOR};
text-decoration: none;
}
`;
================================================
FILE: src/modules/LoginPage/components/index.ts
================================================
export { LoginInfoBlock } from './LoginInfoBlock';
================================================
FILE: src/modules/LoginPage/index.tsx
================================================
import React, { FC } from 'react';
import { useSelector } from 'react-redux';
import { Redirect } from 'react-router-dom';
import { selectToken } from './selectors';
import { StyledLoginImage, StyledLoginPage, StyledLoginPageItemsWrapper } from './styled';
import { LoginInfoBlock } from './components';
export const LoginPage: FC = () => {
const loginToken = useSelector(selectToken);
if (loginToken) return ;
return (
);
};
================================================
FILE: src/modules/LoginPage/loginPageMiddleware.ts
================================================
import { CURRENT_LANG, CURRENT_COURSE, TOUR_OPENING } from 'appConstants';
import { Course } from 'types';
import { setCurrCourse, setCurrLang } from './loginPageReducer';
export const setLanguage = (currentLanguage: string) => {
return (dispatch: (actionCreator: any) => void) => {
localStorage.setItem(CURRENT_LANG, currentLanguage);
dispatch(setCurrLang(currentLanguage));
};
};
export const setCourse = (currentCourse: Course) => {
return (dispatch: (actionCreator: any) => void) => {
localStorage.setItem(CURRENT_COURSE, JSON.stringify(currentCourse));
dispatch(setCurrCourse(currentCourse));
};
};
export const setTourOpening = (tourOpening: string) => {
return () => {
localStorage.setItem(TOUR_OPENING, tourOpening);
};
};
================================================
FILE: src/modules/LoginPage/loginPageReducer.ts
================================================
import {
DEFAULT_LANGUAGE,
SET_COMMON_ERROR,
SET_CURR_COURSE,
SET_CURR_LANG,
SET_TOKEN,
SET_BURGER_MENU_OPEN,
SET_EDIT_PROFILE_DATA_CHANGE,
SET_PATH_TO_THE_PAGE,
SET_IS_TOUR_OPEN,
} from 'appConstants';
import { createActions, handleActions } from 'redux-actions';
import { StateLoginPage } from 'types';
export const loginPageState = {
loginToken: null,
currCourse: {
id: '',
name: '',
teamSize: null,
isActive: true,
},
currLanguage: DEFAULT_LANGUAGE,
isCommonError: false,
isBurgerMenuOpen: false,
isEditProfileDataChange: false,
pathToThePage: '',
isTourOpen: false,
};
export const {
setToken,
setCurrCourse,
setCurrLang,
setCommonError,
setBurgerMenuOpen,
setEditProfileDataChange,
setPathToThePage,
setIsTourOpen,
} = createActions({
SET_TOKEN: (loginToken) => ({ loginToken }),
SET_CURR_COURSE: (currCourse) => ({ currCourse }),
SET_CURR_LANG: (currLanguage) => ({ currLanguage }),
SET_COMMON_ERROR: (isCommonError) => ({ isCommonError }),
SET_BURGER_MENU_OPEN: (isBurgerMenuOpen) => ({ isBurgerMenuOpen }),
SET_EDIT_PROFILE_DATA_CHANGE: (isEditProfileDataChange) => ({
isEditProfileDataChange,
}),
SET_PATH_TO_THE_PAGE: (pathToThePage) => ({
pathToThePage,
}),
SET_IS_TOUR_OPEN: (isTourOpen) => ({
isTourOpen,
}),
});
export const loginPageReducer = handleActions(
{
[SET_TOKEN]: (state, { payload: { loginToken } }) => ({
...state,
loginToken,
}),
[SET_CURR_COURSE]: (state, { payload: { currCourse } }) => ({
...state,
currCourse,
}),
[SET_CURR_LANG]: (state, { payload: { currLanguage } }) => ({
...state,
currLanguage,
}),
[SET_COMMON_ERROR]: (state, { payload: { isCommonError } }) => ({
...state,
isCommonError,
}),
[SET_BURGER_MENU_OPEN]: (state, { payload: { isBurgerMenuOpen } }) => ({
...state,
isBurgerMenuOpen,
}),
[SET_EDIT_PROFILE_DATA_CHANGE]: (state, { payload: { isEditProfileDataChange } }) => ({
...state,
isEditProfileDataChange,
}),
[SET_PATH_TO_THE_PAGE]: (state, { payload: { pathToThePage } }) => ({
...state,
pathToThePage,
}),
[SET_IS_TOUR_OPEN]: (state, { payload: { isTourOpen } }) => ({
...state,
isTourOpen,
}),
},
loginPageState
);
================================================
FILE: src/modules/LoginPage/selectors.ts
================================================
import { State } from 'types';
export const selectToken = (state: State) => state.loginPageReducer.loginToken;
export const selectCurrCourse = (state: State) => state.loginPageReducer.currCourse;
export const selectCurrLanguage = (state: State) => state.loginPageReducer.currLanguage;
export const selectIsCommonError = (state: State) => state.loginPageReducer.isCommonError;
export const selectIsBurgerMenuOpen = (state: State) => state.loginPageReducer.isBurgerMenuOpen;
export const selectIsEditProfileDataChange = (state: State) =>
state.loginPageReducer.isEditProfileDataChange;
export const selectPathToThePage = (state: State) => state.loginPageReducer.pathToThePage;
export const selectIsTourOpen = (state: State) => state.loginPageReducer.isTourOpen;
================================================
FILE: src/modules/LoginPage/styled.ts
================================================
import styled from 'styled-components';
import { MAIN1_COLOR, WHITE_COLOR } from 'appConstants/colors';
import { ReactComponent as LoginImage } from 'assets/svg/loginImage.svg';
export const StyledLoginPage = styled.div`
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100vh;
margin: 0 auto;
background: linear-gradient(90deg, ${WHITE_COLOR} 75%, ${MAIN1_COLOR} 75%);
`;
export const StyledLoginPageItemsWrapper = styled.div`
position: relative;
display: flex;
align-items: center;
width: 100%;
max-width: 1440px;
height: 100%;
overflow: hidden;
`;
export const StyledLoginImage = styled(LoginImage)`
position: absolute;
top: 0;
right: 0;
z-index: -1;
width: auto;
height: 100%;
@media (max-width: 991px) {
left: 50%;
transform: translate(-50%, 0%);
}
`;
================================================
FILE: src/modules/NotFoundPage/index.tsx
================================================
import React, { FC } from 'react';
import { useTranslation } from 'react-i18next';
import { ContentPageWrapper, PageTitle } from 'typography';
import { NotFoundPageWrapper } from './styled';
export const NotFoundPage: FC = () => {
const { t } = useTranslation();
return (
{t('Not found!')}
);
};
================================================
FILE: src/modules/NotFoundPage/styled.ts
================================================
import styled from 'styled-components';
export const NotFoundPageWrapper = styled.div`
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
`;
================================================
FILE: src/modules/StudentsTable/components/Dashboard/components/TableBody/components/TableRow/components/TableItem/index.tsx
================================================
import React, { FC, MouseEvent, useState } from 'react';
import { TablePopup } from 'components/TablePopup';
import { StyledTableItem } from './styled';
type TableItemProps = {
item: string;
index: number;
dataLength: number;
};
export const TableItem: FC = ({ item, index, dataLength }) => {
const [tableItemCursor, setTableItemCursor] = useState(false);
const [showPopup, setShowPopup] = useState(false);
const [popupElements, setPopupElements] = useState([]);
const mouseOverHandler = (event: MouseEvent) => {
const target = event.target as HTMLDivElement;
if (target.scrollWidth !== target.clientWidth) {
setShowPopup(true);
setPopupElements(target?.textContent?.split(',') as string[]);
setTableItemCursor(true);
}
};
const mouseLeaveHandler = () => {
setShowPopup(false);
setPopupElements([]);
setTableItemCursor(false);
};
return (
{item}
{showPopup && }
);
};
================================================
FILE: src/modules/StudentsTable/components/Dashboard/components/TableBody/components/TableRow/components/TableItem/styled.ts
================================================
import styled from 'styled-components';
type TStyledTableItem = {
tableItemCursor: boolean;
};
export const StyledTableItem = styled.div`
position: relative;
max-width: 140px;
cursor: ${({ tableItemCursor }) => (tableItemCursor ? 'pointer' : 'unset')};
.TableItem__first-element {
width: 100%;
margin: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
`;
================================================
FILE: src/modules/StudentsTable/components/Dashboard/components/TableBody/components/TableRow/index.tsx
================================================
import React, { FC } from 'react';
import { ListChildComponentProps } from 'react-window';
import { TableItem } from './components/TableItem';
import { StyledTableRow } from './styled';
export const TableRow: FC = ({ data, index, style }) => {
const optimalItemIndexCount = 2;
return (
{data[index].map((item: string, ind: number) => (
optimalItemIndexCount ? index / (data.length - 1) : 0}
key={`TableItemKey-${ind}`}
/>
))}
);
};
================================================
FILE: src/modules/StudentsTable/components/Dashboard/components/TableBody/components/TableRow/styled.ts
================================================
import { BG_COLOR, DARK_TEXT_COLOR } from 'appConstants/colors';
import styled from 'styled-components';
export const StyledTableRow = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
font-weight: 400;
line-height: 150%;
color: ${DARK_TEXT_COLOR};
border-radius: 10px;
&:nth-child(2n) {
background-color: ${BG_COLOR};
}
@media (max-width: 768px) {
padding: 10px;
}
@media (max-width: 440px) {
padding: 5px;
}
`;
================================================
FILE: src/modules/StudentsTable/components/Dashboard/components/TableBody/index.tsx
================================================
import React, { FC, useMemo, ReactText } from 'react';
import { useSelector } from 'react-redux';
import { FixedSizeList as List } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer';
import { StyledTableBody } from './styled';
import './styles.css';
import { User, Course, Team } from 'types';
import { USERS_PER_PAGE } from 'appConstants';
import { TableRow } from './components/TableRow';
import { selectCurrCourse } from 'modules/LoginPage/selectors';
import { useTranslation } from 'react-i18next';
type TableBodyProps = {
users: User[];
page: number;
};
export const TableBody: FC = ({ users, page }) => {
const currCourse = useSelector(selectCurrCourse);
const { t } = useTranslation();
const usersData: Array = useMemo(
() =>
users.map((user: User, index: number) => {
return [
`${index + 1 + page * USERS_PER_PAGE}`,
`${user.firstName} ${user.lastName || null}`,
`${user.score}`,
user.teams.find((team: Team) => team.courseId === currCourse.id)
? `${user.teams.find((team: Team) => team.courseId === currCourse.id)?.number}`
: (t('No team yet.') as string),
user.telegram || `${t('No')} telegram.`,
user.discord || `${t('No')} discord.`,
user.github || `${t('No')} GitHub.`,
`${user.country},
${user.city}`,
user.courses.length
? user.courses.map((course: Course) => course.name).join(', ')
: (t('No courses.') as string),
];
}),
[users, page, currCourse.id, t]
);
return (
{({ height, width }) => (
{TableRow}
)}
);
};
================================================
FILE: src/modules/StudentsTable/components/Dashboard/components/TableBody/styled.ts
================================================
import styled from 'styled-components';
export const StyledTableBody = styled.div`
display: flex;
width: 100%;
height: 100%;
`;
================================================
FILE: src/modules/StudentsTable/components/Dashboard/components/TableBody/styles.css
================================================
.List {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
overflow-y: scroll !important;
}
.List::-webkit-scrollbar {
width: 10px;
background-color: var(--WHITE_COLOR);
}
.List::-webkit-scrollbar-thumb {
background-color: var(--SCROLL_THUMB_COLOR);
border-radius: 20px;
}
.TableItem--0 {
width: 3.5%;
}
.TableItem--1 {
width: 16%;
}
.TableItem--2 {
width: 5%;
}
.TableItem--3 {
width: 14%;
}
.TableItem--4,
.TableItem--5,
.TableItem--6 {
width: 9.93%;
}
.TableItem--7, .TableItem--8 {
width: 14%;
}
.TableItem--9 {
width: 11%;
}
@media (max-width: 1320px) {
.TableItem--0 {
width: 4%;
}
.TableItem--1 {
width: 16%;
}
.TableItem--2 {
width: 6%;
}
.TableItem--3 {
width: 14%;
}
.TableItem--8 {
width: 15%;
}
}
@media (max-width: 1200px) {
.TableRow {
font-size: 0.95rem;
}
.TableItem--0 {
width: 4.5%;
}
.TableItem--1 {
width: 18%;
}
.TableItem--2 {
width: 6%;
}
.TableItem--3 {
width: 15%;
}
.TableItem--7 {
display: none;
}
.TableItem--8 {
width: 15%;
}
}
@media (max-width: 992px) {
.TableRow {
font-size: 0.9rem;
}
.TableItem--0 {
width: 6%;
}
.TableItem--1 {
width: 20.5%;
}
.TableItem--2 {
width: 7%;
}
.TableItem--3 {
width: 17%;
}
.TableItem--4 {
width: 11%;
}
.TableItem--5,
.TableItem--7 {
display: none;
}
.TableItem--8 {
width: 20%;
}
}
@media (max-width: 768px) {
.TableRow {
padding: 10px;
font-size: 0.825rem;
}
.TableItem--0 {
width: 9%;
}
.TableItem--1 {
width: 28%;
}
.TableItem--3 {
width: 25%;
}
.TableItem--4 {
width: 17.5%;
}
.TableItem--2,
.TableItem--5,
.TableItem--7,
.TableItem--6 {
display: none;
}
.TableItem--8 {
width: 20.5%;
}
}
@media (max-width: 550px) {
.TableRow {
padding: 10px;
font-size: 0.8rem;
}
.TableItem--0 {
width: 11%;
}
.TableItem--1 {
width: 33%;
}
.TableItem--4 {
width: 21.5%;
}
.TableItem--2,
.TableItem--3,
.TableItem--5,
.TableItem--7,
.TableItem--6 {
display: none;
}
.TableItem--8 {
width: 27.5%;
}
}
@media (max-width: 440px) {
.TableRow {
padding: 5px;
font-size: 0.68rem;
}
.TableItem--0 {
width: 12%;
}
.TableItem--1 {
width: 36%;
}
.TableItem--4 {
width: 23%;
}
.TableItem--2,
.TableItem--3,
.TableItem--5,
.TableItem--7,
.TableItem--6 {
display: none;
}
.TableItem--8 {
width: 24.5%;
}
}
================================================
FILE: src/modules/StudentsTable/components/Dashboard/components/TableHead/index.tsx
================================================
import React, { FC } from 'react';
import { TABLE_HEADERS } from 'appConstants';
import { StyledTableHead, StyledTableHeadRow, StyledTableHeader } from './styled';
import { useTranslation } from 'react-i18next';
export const TableHead: FC = () => {
const { t } = useTranslation();
return (
{TABLE_HEADERS.map((tableHeader: string, index: number) => (
{t(tableHeader)}
))}
);
};
================================================
FILE: src/modules/StudentsTable/components/Dashboard/components/TableHead/styled.ts
================================================
import styled from 'styled-components';
import { LIGHT_TEXT_COLOR, DASHBOARD_HEADER_BG_COLOR } from 'appConstants/colors';
export const StyledTableHead = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-right: 10px;
padding: 10px 20px;
color: ${LIGHT_TEXT_COLOR};
background-color: ${DASHBOARD_HEADER_BG_COLOR};
border-radius: 10px;
@media (max-width: 768px) {
padding: 10px;
}
@media (max-width: 440px) {
padding: 5px;
}
`;
export const StyledTableHeadRow = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
`;
export const StyledTableHeader = styled.div`
max-width: 140px;
height: auto;
margin: 0;
font-weight: 600;
line-height: 150%;
text-align: start;
`;
================================================
FILE: src/modules/StudentsTable/components/Dashboard/components/index.ts
================================================
export { TableBody } from './TableBody';
export { TableHead } from './TableHead';
================================================
FILE: src/modules/StudentsTable/components/Dashboard/index.tsx
================================================
import React, { FC } from 'react';
import { TableBody, TableHead } from './components';
import { StyledTable } from './styled';
import { User } from 'types';
type DashboardProps = {
users: User[];
page: number;
};
export const Dashboard: FC = ({ users, page }) => {
return (
);
};
================================================
FILE: src/modules/StudentsTable/components/Dashboard/styled.ts
================================================
import styled from 'styled-components';
import { WHITE_COLOR } from 'appConstants/colors';
import { GeneralAdaptiveFont } from 'typography';
export const StyledTable = styled.div`
display: flex;
flex-direction: column;
width: 100%;
max-width: 1320px;
height: 75vh;
margin: 0 auto;
padding: 10px;
padding-right: 0;
font-size: 1rem;
background-color: ${WHITE_COLOR};
border-radius: 20px;
${GeneralAdaptiveFont};
`;
================================================
FILE: src/modules/StudentsTable/index.tsx
================================================
import React, { FC, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { USERS_PER_PAGE } from 'appConstants';
import { Loader, Pagination, FilterForm, ErrorModal } from 'components';
import { useUsersQuery } from 'hooks/graphql';
import { selectCurrCourse } from 'modules/LoginPage/selectors';
import { Dashboard } from './components/Dashboard';
import { StudentTableWrapper, TableTitle } from './styled';
import { TeamsTitleWrapper } from 'modules/TeamsList/styled';
import { FilterButton } from 'components/FilterForm/styled';
import filterIcon from 'assets/svg/filterIcon.svg';
import crossIcon from 'assets/svg/cross.svg';
import { WHITE_COLOR } from 'appConstants/colors';
import { defaultFilterData, filterSelectFields } from 'components/FilterForm/filterFormFields';
import { selectFilterData } from './selectors';
import { TFilterForm } from 'types';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { setFilterData } from './studentsTableReducer';
import { ContentPageWrapper } from 'typography';
export const StudentsTable: FC = () => {
const [page, setPage] = useState(0);
const [isFilterOpen, setIsFilterOpen] = useState(false);
const [inputValues, setInputValues] = useState(defaultFilterData);
const { t } = useTranslation();
const { register, handleSubmit, errors, reset } = useForm({
defaultValues: inputValues,
mode: 'onChange',
});
const dispatch = useDispatch();
const currCourse = useSelector(selectCurrCourse);
const filterData = useSelector(selectFilterData);
const { loadingU, errorU, users } = useUsersQuery({
filter: {
...filterData,
sortingOrder: (
filterSelectFields[0][1].find((item) => item[0] === filterData.sortingOrder) as string[]
)[1],
teamFilter: (
filterSelectFields[1][1].find((item) => item[0] === filterData.teamFilter) as [
string,
boolean
]
)[1],
},
reactCourseId: currCourse.id,
page,
});
const loading = loadingU;
const error = errorU;
if (error) return ;
if (loading) return ;
const pageCount: number = Math.ceil(users.count / USERS_PER_PAGE);
const isValuesEqual =
Object.values(defaultFilterData).toString() !== Object.values(filterData).toString();
const onClickClearBtnHandler = () => {
setInputValues(defaultFilterData);
dispatch(setFilterData(defaultFilterData));
setPage(0);
setIsFilterOpen(false);
};
const onClickOpenFilterBtnHandler = () => {
setIsFilterOpen(!isFilterOpen);
reset(filterData);
setInputValues(filterData);
};
return (
{t('Dashboard')}
{isValuesEqual && !isFilterOpen && (
{
}
{t('Clear filter')}
)}
{
} {t('Filter')}
{isFilterOpen && (
)}
{!!users.results.length && (
)}
);
};
================================================
FILE: src/modules/StudentsTable/selectors.ts
================================================
import { State } from 'types';
export const selectUserData = (state: State) => state.studentsTableReducer.userData;
export const selectFilterData = (state: State) => state.studentsTableReducer.filterData;
================================================
FILE: src/modules/StudentsTable/studentsTableReducer.ts
================================================
import { SET_USER_DATA, SET_FILTER_DATA } from 'appConstants';
import { StateStudentsTable } from 'types';
import { defaultFilterData } from 'components/FilterForm/filterFormFields';
import { createActions, handleActions } from 'redux-actions';
export const studentsTableState = {
userData: {
id: '',
firstName: '',
lastName: '',
github: '',
telegram: null,
discord: '',
score: 1000,
country: '',
city: '',
avatar: '',
isAdmin: false,
courses: [],
email: '',
courseIds: [''],
teamIds: [''],
teams: [],
},
filterData: defaultFilterData,
};
export const { setUserData, setFilterData } = createActions({
SET_USER_DATA: (userData) => ({ userData }),
SET_FILTER_DATA: (filterData) => ({ filterData }),
});
export const studentsTableReducer = handleActions(
{
[SET_USER_DATA]: (state, { payload: { userData } }) => ({
...state,
userData,
}),
[SET_FILTER_DATA]: (state, { payload: { filterData } }) => ({
...state,
filterData,
}),
},
studentsTableState
);
================================================
FILE: src/modules/StudentsTable/styled.ts
================================================
import styled from 'styled-components';
import { DARK_TEXT_COLOR } from 'appConstants/colors';
import { ScrollBar } from 'typography';
export const StudentTableWrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
height: fit-content;
max-width: 1440px;
padding: 0 4% 60px;
overflow-y: scroll;
${ScrollBar};
@media screen and (max-width: 768px) {
padding-bottom: 50px;
}
`;
export const TableTitle = styled.h1`
align-self: flex-start;
width: 80%;
margin: 40px 0;
font: 700 30px/45px 'Poppins', sans-serif;
color: ${DARK_TEXT_COLOR};
@media (max-width: 1200px) {
font-size: 26px;
line-height: 35px;
}
@media (max-width: 992px) {
margin: 35px 0;
font-size: 24px;
line-height: 30px;
}
@media (max-width: 768px) {
margin: 30px 0;
font-size: 22px;
line-height: 25px;
}
@media (max-width: 550px) {
margin: 30px 0;
font-size: 20px;
line-height: 25px;
}
@media (max-width: 440px) {
margin: 20px 0;
font-size: 17px;
line-height: 20px;
}
`;
================================================
FILE: src/modules/TeamsList/components/TeamListModals/index.tsx
================================================
import React, { FC, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { ModalExpel, ModalJoin, ModalCreateEditTeam, ModalCreated } from 'components';
import { MODAL_INPUT_VALIDATION } from 'appConstants';
import {
selectIsActiveModalCreated,
selectIsActiveModalCreateTeam,
selectIsActiveModalExpel,
selectIsActiveModalJoin,
selectIsActiveModalLeave,
selectIsActiveModalRemoveCourse,
selectIsActiveModalUpdateSocialLink,
selectSocialLink,
selectTeamPassword,
} from '../../selectors';
import { Team, User } from 'types';
import { setUserData } from 'modules/StudentsTable/studentsTableReducer';
import {
setSocialLink,
setTeamPassword,
activeModalExpel,
activeModalLeave,
activeModalJoin,
activeModalCreateTeam,
activeModalCreated,
activeModalUpdateSocialLink,
activeModalRemoveCourse,
} from 'modules/TeamsList/teamsListReducer';
type TeamListModalsProps = {
addUserToTeam: any;
removeUserFromTeam: any;
expelUserFromTeam: any;
createTeam: any;
updateTeam: any;
removeUserFromCourse: any;
};
export const TeamListModals: FC = ({
addUserToTeam,
removeUserFromTeam,
expelUserFromTeam,
createTeam,
updateTeam,
removeUserFromCourse,
}) => {
const [textJoinModal, setTextJoinModal] = useState('Please, enter your team password.');
const dispatch = useDispatch();
const isActiveModalExpel = useSelector(selectIsActiveModalExpel);
const isActiveModalLeave = useSelector(selectIsActiveModalLeave);
const isActiveModalJoin = useSelector(selectIsActiveModalJoin);
const isActiveModalCreateTeam = useSelector(selectIsActiveModalCreateTeam);
const isActiveModalCreated = useSelector(selectIsActiveModalCreated);
const isActiveModalUpdateSocialLink = useSelector(selectIsActiveModalUpdateSocialLink);
const isActiveModalRemoveCourse = useSelector(selectIsActiveModalRemoveCourse);
const teamPassword = useSelector(selectTeamPassword);
const socialLink = useSelector(selectSocialLink);
const onSubmitJoinModal = async (e: string) => {
addUserToTeam().then(({ data: { addUserToTeam } }: { data: { addUserToTeam: User } }) => {
const isPasswordIncorrect =
!addUserToTeam.teams || !addUserToTeam.teams.find((team: Team) => team.password === e);
if (isPasswordIncorrect) {
setTextJoinModal('Wrong password!');
} else {
setTextJoinModal('Please, enter your team password.');
dispatch(setUserData(addUserToTeam));
dispatch(activeModalJoin(false));
dispatch(setTeamPassword(''));
}
});
};
const onSubmitLeaveModal = () => {
removeUserFromTeam().then(
({ data: { removeUserFromTeam } }: { data: { removeUserFromTeam: User } }) => {
dispatch(setUserData(removeUserFromTeam));
}
);
};
const onSubmitExpelModal = () => {
expelUserFromTeam();
};
const onSubmitRemoveCourseModal = () => {
removeUserFromCourse();
};
const onSubmitCreateTeam = () => {
createTeam().then(({ data: { createTeam } }: { data: { createTeam: Team } }) => {
dispatch(setTeamPassword(createTeam.password));
dispatch(activeModalCreated(true));
});
};
const onSubmitUpdateSocialLink = () => {
updateTeam();
};
return (
<>
dispatch(activeModalLeave(false))}
okText="Yes"
cancelText="No"
/>
dispatch(activeModalExpel(false))}
okText="Yes"
cancelText="No"
/>
dispatch(activeModalRemoveCourse(false))}
okText="Yes"
cancelText="No"
/>
{/*Create team*/}
{
dispatch(activeModalCreateTeam(false));
dispatch(setSocialLink(''));
}}
okText="Create team"
validateRules={MODAL_INPUT_VALIDATION}
/>
setTextJoinModal('Please, enter your team password.')}
onClose={() => {
setTextJoinModal('Please, enter your team password.');
dispatch(activeModalJoin(false));
dispatch(setTeamPassword(''));
}}
okText="Join team"
/>
dispatch(activeModalCreated(false))}
cancelText="Got it!"
password={teamPassword}
/>
{/*Edit Team*/}
{
dispatch(activeModalUpdateSocialLink(false));
dispatch(setSocialLink(''));
}}
okText="Update link"
/>
>
);
};
================================================
FILE: src/modules/TeamsList/components/TeamListModals/useCommonMutations.ts
================================================
import { useSelector } from 'react-redux';
import { selectSocialLink, selectTeamMemberExpelId, selectTeamPassword } from '../../selectors';
import { Team } from 'types';
import {
useAddUserToTeamMutation,
useRemoveUserFromTeamMutation,
useExpelUserFromTeamMutation,
useCreateTeamMutation,
useUpdateTeamMutation,
useRemoveUserFromCourseMutation,
} from 'hooks/graphql';
import { selectCurrCourse } from 'modules/LoginPage/selectors';
import { selectUserData } from 'modules/StudentsTable/selectors';
export const useCommonMutations = (page: number) => {
const currCourse = useSelector(selectCurrCourse);
const userData = useSelector(selectUserData);
const teamMemberId = useSelector(selectTeamMemberExpelId);
const teamPassword = useSelector(selectTeamPassword);
const socialLink = useSelector(selectSocialLink);
const { addUserToTeam, errorM: errorAdd } = useAddUserToTeamMutation({
data: {
userId: userData.id,
courseId: currCourse.id,
teamPassword,
},
});
const {
removeUserFromTeam,
errorM: errorRemoveTeam,
loadingM: loadingRemoveTeam,
} = useRemoveUserFromTeamMutation({
data: {
teamId: userData.teams.find((team: Team) => team.courseId === currCourse.id)?.id ?? '',
userId: userData.id,
courseId: currCourse.id,
page,
},
});
const {
expelUserFromTeam,
errorM: errorExpel,
loadingM: loadingExpel,
} = useExpelUserFromTeamMutation({
data: {
teamId: userData.teams.find((team: Team) => team.courseId === currCourse.id)?.id ?? '',
userId: teamMemberId,
courseId: currCourse.id,
page,
},
});
const {
createTeam,
errorM: errorCreateTeam,
loadingM: loadingCreateTeam,
} = useCreateTeamMutation({
team: {
socialLink,
courseId: currCourse.id,
ownerId: userData.id,
page,
},
});
const {
updateTeam,
errorM: errorUpdateTeam,
loadingM: loadingUpdateTeam,
} = useUpdateTeamMutation({
team: {
socialLink,
id: userData.teams.find((team: Team) => team.courseId === currCourse.id)?.id ?? '',
},
});
const {
removeUserFromCourse,
errorM: errorRemoveCourse,
loadingM: loadingRemoveCourse,
} = useRemoveUserFromCourseMutation({
data: {
courseId: currCourse.id,
userId: userData.id,
teamId: userData.teams.find((team: Team) => team.courseId === currCourse.id)?.id ?? null,
page,
},
});
const commonMutationError = [
errorAdd,
errorCreateTeam,
errorExpel,
errorRemoveCourse,
errorRemoveTeam,
errorUpdateTeam,
].find((item) => !!item);
const isLoading = [
loadingCreateTeam,
loadingExpel,
loadingRemoveCourse,
loadingRemoveTeam,
loadingUpdateTeam,
].some((item) => !!item);
return {
addUserToTeam,
removeUserFromTeam,
expelUserFromTeam,
createTeam,
updateTeam,
removeUserFromCourse,
commonMutationError,
isLoading,
};
};
================================================
FILE: src/modules/TeamsList/components/Teams/components/MemberListToggle/index.tsx
================================================
import React, { FC } from 'react';
import { MembersListToggleStyled, Chevron } from './styled';
import { ReactComponent as ChevronArrow } from 'assets/svg/chevron-arrow.svg';
import { useTranslation } from 'react-i18next';
type MembersListToggle = {
countMembers: number;
isOpen: boolean;
onToggleList: () => void;
color?: string;
};
export const MembersListToggle: FC = ({
countMembers,
isOpen,
onToggleList,
color,
}) => {
const { t } = useTranslation();
return (
{
!!countMembers && onToggleList();
}}
color={color}
>
{countMembers || 0} {countMembers === 1 ? t('member') : t('members')}
);
};
================================================
FILE: src/modules/TeamsList/components/Teams/components/MemberListToggle/styled.ts
================================================
import styled from 'styled-components';
import { WHITE_COLOR } from 'appConstants/colors';
import { SVGArrowAdaptive } from 'typography';
type ChevronProps = {
open: boolean;
};
export const MembersListToggleStyled = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
width: 134px;
cursor: pointer;
color: ${({ color }) => color || WHITE_COLOR};
path {
stroke: currentColor;
}
@media (max-width: 992px) {
width: 115px;
}
@media (max-width: 768px) {
width: 105px;
}
@media (max-width: 440px) {
width: 85px;
}
`;
export const Chevron = styled.div`
display: flex;
justify-content: center;
align-items: center;
transform: rotateX(${({ open }) => (open ? '180deg' : '0deg')});
margin-left: 10px;
transition: all 0.3s;
svg {
${SVGArrowAdaptive};
}
`;
================================================
FILE: src/modules/TeamsList/components/Teams/components/MyTeam/components/MyTeamInfoBlock/components/MyTeamInfoLine/index.tsx
================================================
import React, { FC, MouseEvent, useState } from 'react';
import { InfoLineStyled, CopyClipboardButton } from './styled';
import { TablePopup } from 'components/TablePopup';
type MyTeamInfoLine = {
value?: string;
hoverHandler?: () => void;
};
export const MyTeamInfoLine: FC = ({ value }) => {
const [showPopup, setShowPopup] = useState(false);
const [isCopy, setIsCopy] = useState(false);
const copyInfo = (value: string) => {
navigator.clipboard
.writeText(value)
.then(() => {
setIsCopy(true);
setTimeout(() => {
setIsCopy(false);
}, 1000);
})
.catch((err) => {
console.log(err);
});
};
const mouseOverHandler = (event: MouseEvent) => {
const target = event.target as HTMLDivElement;
if (target.scrollWidth !== target.clientWidth) {
setShowPopup(true);
}
};
const currValue: string = value || '';
return (
setShowPopup(false)}
>
{currValue}
{showPopup && }
copyInfo(currValue)} />
);
};
================================================
FILE: src/modules/TeamsList/components/Teams/components/MyTeam/components/MyTeamInfoBlock/components/MyTeamInfoLine/styled.tsx
================================================
import styled from 'styled-components';
import { WHITE_COLOR } from 'appConstants/colors';
import { TextBold, GeneralAdaptiveFont, SVGParamsAdaptive } from 'typography';
import { ReactComponent as CopyIcon } from 'assets/svg/copy.svg';
type TInfoLineStyled = {
blink: boolean;
};
export const InfoLineStyled = styled.div`
${TextBold};
color: ${WHITE_COLOR};
display: flex;
align-items: center;
border-radius: 5px;
.info__text {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
animation: ${({ blink }) => (blink ? 'blink 1s' : 'none')};
border-radius: 3px;
padding: 0 7px;
margin-left: -7px;
${GeneralAdaptiveFont};
}
@keyframes blink {
from {
background: rgb(101, 80, 246, 0.5);
}
to {
background: rgba(101, 80, 246, 0);
}
}
`;
export const CopyClipboardButton = styled(CopyIcon)`
cursor: pointer;
margin-left: 3px;
${SVGParamsAdaptive};
`;
================================================
FILE: src/modules/TeamsList/components/Teams/components/MyTeam/components/MyTeamInfoBlock/components/NotificationPopup/index.tsx
================================================
import React, { FC } from 'react';
import { NotificationPopupStyled } from './styled';
const NotificationPopup: FC = ({ children }) => {
return {children};
};
export default NotificationPopup;
================================================
FILE: src/modules/TeamsList/components/Teams/components/MyTeam/components/MyTeamInfoBlock/components/NotificationPopup/styled.ts
================================================
import styled from 'styled-components';
import { LIGHT_TEXT_COLOR, WHITE_COLOR } from 'appConstants/colors';
export const NotificationPopupStyled = styled.div`
position: absolute;
top: 153%;
right: 0;
width: 240px;
background-color: ${WHITE_COLOR};
color: ${LIGHT_TEXT_COLOR};
border-radius: 10px;
padding: 20px 20px;
z-index: 1;
&::after {
content: '';
position: absolute;
top: 0;
right: 8px;
transform: rotateZ(59deg) skew(33deg);
height: 22px;
width: 22px;
border-radius: 4px;
background-color: ${WHITE_COLOR};
}
`;
================================================
FILE: src/modules/TeamsList/components/Teams/components/MyTeam/components/MyTeamInfoBlock/index.tsx
================================================
import React, { FC, useState } from 'react';
import { useDispatch } from 'react-redux';
import { StyledMyTeamInfoBlock, InfoButton } from './styled';
import { MyTeamInfoLine } from './components/MyTeamInfoLine';
import { ReactComponent as InfoIcon } from 'assets/svg/info.svg';
import { ReactComponent as EditIcon } from 'assets/svg/edit.svg';
import NotificationPopup from './components/NotificationPopup';
import { useTranslation } from 'react-i18next';
import { activeModalUpdateSocialLink } from 'modules/TeamsList/teamsListReducer';
type MyTeamInfoBlockProps = {
title: string;
icon: 'info' | 'edit';
value: string;
};
export const MyTeamInfoBlock: FC = ({ title, icon, value }) => {
const dispatch = useDispatch();
const [hover, setHover] = useState(false);
const { t } = useTranslation();
return (
{t(title)}
{icon === 'info' ? (
setHover(true)} onMouseOut={() => setHover(false)}>
{hover && (
{t('The password is required to join the team.')}
)}
) : (
dispatch(activeModalUpdateSocialLink(true))}>
)}
);
};
================================================
FILE: src/modules/TeamsList/components/Teams/components/MyTeam/components/MyTeamInfoBlock/styled.ts
================================================
import styled, { css } from 'styled-components';
import { WHITE_COLOR, MAIN2_LIGHT_COLOR, MAIN2_COLOR } from 'appConstants/colors';
import { TextRegular, GeneralAdaptiveFont, SVGParamsAdaptive } from 'typography';
export const StyledMyTeamInfoBlock = styled.div`
position: relative;
max-width: 300px;
width: 100%;
border-radius: 10px;
padding: 20px;
color: ${WHITE_COLOR};
background-color: ${MAIN2_LIGHT_COLOR};
.infoBlock__title {
${TextRegular};
color: ${WHITE_COLOR};
margin-bottom: 10px;
${GeneralAdaptiveFont};
}
@media (max-width: 992px) {
max-width: 50%;
}
@media (max-width: 580px) {
max-width: 100%;
}
`;
const InfoBlockButton = css`
position: absolute;
top: 10px;
right: 10px;
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
padding: 8px;
background: ${MAIN2_COLOR};
fill: ${WHITE_COLOR};
cursor: pointer;
border-radius: 5px;
`;
export const InfoButton = styled.div`
${InfoBlockButton};
svg {
${SVGParamsAdaptive};
}
`;
================================================
FILE: src/modules/TeamsList/components/Teams/components/MyTeam/index.tsx
================================================
import React, { FC, useState } from 'react';
import { useDispatch } from 'react-redux';
import { StyledMyTeam, HeaderDecor, TableWrapper } from './styled';
import { TeamButton } from 'typography';
import { DARK_TEXT_COLOR, WHITE_COLOR } from 'appConstants/colors';
import { Team } from 'types';
import { MembersListToggle } from '../MemberListToggle';
import { MyTeamInfoBlock } from './components/MyTeamInfoBlock';
import { TeamUserTable } from '../TeamUserTable';
import { useTranslation } from 'react-i18next';
import { activeModalLeave, activeModalRemoveCourse } from 'modules/TeamsList/teamsListReducer';
type MyTeamProps = {
team: Team;
userId: string;
};
export const MyTeam: FC = ({ team, userId }) => {
const [isOpen, setOpenState] = useState(false);
const { t } = useTranslation();
const dispatch = useDispatch();
const toggleListHandler = () => setOpenState(!isOpen);
const leaveTeam = () => dispatch(activeModalLeave(true));
const removeCourse = () => dispatch(activeModalRemoveCourse(true));
const countMember = team?.members?.length;
return (
{t('My team - Team')} {team.number}
{t('Leave course')}
{t('Leave team')}
{isOpen && }
);
};
================================================
FILE: src/modules/TeamsList/components/Teams/components/MyTeam/styled.ts
================================================
import styled from 'styled-components';
import { ReactComponent as HeaderDecoration } from 'assets/svg/team-header-decorations.svg';
import { MAIN2_COLOR, WHITE_COLOR } from 'appConstants/colors';
import { PageSubTitle } from 'typography';
type StyledMyTeamProps = {
open: boolean;
};
type TableWrapperProps = {
open: boolean;
};
export const StyledMyTeam = styled.div`
margin-bottom: 40px;
.myTeam__header {
position: relative;
color: ${WHITE_COLOR};
background-color: ${MAIN2_COLOR};
border-radius: 20px;
z-index: 0;
padding: 30px;
display: grid;
grid-template-columns: 1fr 215px 215px;
grid-template-rows: auto 1fr;
grid-template-areas: 'title leave button' 'info info toggle';
@media (max-width: 992px) {
grid-template-columns: 1fr 200px 200px;
grid-template-rows: auto auto auto;
grid-template-areas: 'title leave button' 'info info info' 'toggle toggle toggle';
font-size: 0.9rem;
}
@media (max-width: 650px) {
grid-template-columns: 1fr 100px 187px;
grid-template-rows: auto auto auto auto;
grid-template-areas: 'title title button' 'title title leave' 'info info info' 'toggle toggle toggle';
font-size: 0.825rem;
}
@media (max-width: 440px) {
grid-template-columns: 1fr 115px 160px;
font-size: 0.68rem;
}
@media (max-width: 350px) {
padding: 20px;
grid-template-columns: 1fr 60px 145px;
grid-template-rows: auto auto auto auto;
}
@media (max-width: 580px) {
overflow: hidden;
}
.myTeam__title {
grid-area: title;
${PageSubTitle};
margin: 5px 0 37px;
@media (max-width: 550px) {
font-size: 0.9rem;
}
}
.myTeam__info-wrapper {
grid-area: info;
display: flex;
gap: 30px;
@media (max-width: 580px) {
flex-direction: column;
}
}
.myTeam__leave,
.myTeam__button {
display: flex;
justify-content: flex-end;
align-items: flex-start;
margin: 0 0 10px 10px;
button {
width: 100%;
}
@media (max-width: 650px) {
margin: 0 0 10px;
}
@media (max-width: 440px) {
margin: 0 0 10px 10px;
}
}
.myTeam__leave {
grid-area: leave;
}
.myTeam__button {
grid-area: button;
}
.myTeam__toggle {
grid-area: toggle;
display: flex;
justify-content: flex-end;
align-items: flex-end;
@media (max-width: 992px) {
margin-top: 20px;
}
}
}
.myTeam__table-wrapper {
background-color: ${WHITE_COLOR};
margin-bottom: ${({ open }) => (open ? '40px' : '0')};
margin-top: ${({ open }) => (open ? '-20px' : '0')};
${({ open }) => (open ? 'padding: 60px 30px 30px' : null)};
border-bottom-left-radius: 20px;
border-bottom-right-radius: 20px;
transition: all 0.3s;
}
`;
export const TableWrapper = styled.div`
background-color: ${WHITE_COLOR};
margin-bottom: ${({ open }) => (open ? '40px' : '0')};
margin-top: ${({ open }) => (open ? '-20px' : '0')};
${({ open }) => (open ? 'padding: 60px 30px 30px' : null)};
border-bottom-left-radius: 20px;
border-bottom-right-radius: 20px;
transition: all 0.3s;
@media screen and (max-width: 768px) {
overflow-x: ${({ open }) => (open ? 'scroll' : 'auto')};
}
@media (max-width: 440px) {
${({ open }) => (open ? 'padding: 40px 15px 30px' : null)};
}
`;
export const HeaderDecor = styled(HeaderDecoration)`
top: 0;
width: 220px;
height: 160px;
position: absolute;
right: 0;
z-index: -1;
`;
================================================
FILE: src/modules/TeamsList/components/Teams/components/TeamItem/index.tsx
================================================
import React, { FC, useState } from 'react';
import { TeamItemStyled, TeamItemTableWrapper } from './styled';
import { User } from 'types';
import { MembersListToggle } from '../MemberListToggle';
import { LIGHT_TEXT_COLOR } from 'appConstants/colors';
import { TeamUserTable } from '../TeamUserTable';
type TeamItemProps = {
name: string;
description?: string;
countMember: number;
members: User[];
};
export const TeamItem: FC = ({ name, countMember, description, members }) => {
const [isOpen, setToggle] = useState(false);
const toggleListHandler = () => {
setToggle(!isOpen);
};
return (
{name}
{description &&
{description}
}
{isOpen && }
);
};
================================================
FILE: src/modules/TeamsList/components/Teams/components/TeamItem/styled.tsx
================================================
import styled from 'styled-components';
import { TextRegular, TextSemiBold, GeneralAdaptiveFont } from 'typography';
import { WHITE_COLOR } from 'appConstants/colors';
import { TableWrapper } from '../MyTeam/styled';
export const TeamItemStyled = styled.div`
background: ${WHITE_COLOR};
border-radius: 20px;
margin-bottom: 20px;
.teamItem__header {
padding: 20px 30px;
display: flex;
align-items: center;
justify-content: space-between;
${GeneralAdaptiveFont};
}
.teamItem__name {
${TextSemiBold};
${GeneralAdaptiveFont};
}
.teamItem__description {
${TextRegular};
flex-grow: 1;
}
`;
export const TeamItemTableWrapper = styled(TableWrapper)`
margin-top: ${({ open }) => (open ? '-20px' : '0')};
${({ open }) => (open ? 'padding: 20px 30px 20px' : null)};
@media (max-width: 440px) {
${({ open }) => (open ? 'padding: 20px 15px 20px' : null)};
}
`;
================================================
FILE: src/modules/TeamsList/components/Teams/components/TeamUserTable/components/TableRow/components/ExpelButton/index.tsx
================================================
import React, { FC } from 'react';
import { SmallCrossIcon, StyledExpelButton } from './styled';
type ExpelButtonProps = {
onClickHandler?: () => void;
};
export const ExpelButton: FC = ({ onClickHandler }) => {
return (
Expel
);
};
================================================
FILE: src/modules/TeamsList/components/Teams/components/TeamUserTable/components/TableRow/components/ExpelButton/styled.ts
================================================
import styled from 'styled-components';
import { ReactComponent as Cross } from 'assets/svg/cross-small.svg';
import { MAIN1_COLOR } from 'appConstants/colors';
import { SVGArrowAdaptive } from 'typography';
type StyledExpelButtonProps = {
color?: string;
};
export const StyledExpelButton = styled.div`
width: 63px;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
color: ${({ color }) => (color ? color : MAIN1_COLOR)};
@media (max-width: 550px) {
width: 55px;
}
`;
export const SmallCrossIcon = styled(Cross)`
width: 12px;
height: 12px;
${SVGArrowAdaptive};
@media (max-width: 992px) and (min-width: 768px) {
width: 11px;
height: 11px;
}
path {
stroke: currentColor;
}
`;
================================================
FILE: src/modules/TeamsList/components/Teams/components/TeamUserTable/components/TableRow/components/TableCell/index.tsx
================================================
import React, { FC } from 'react';
type TTableCell = {
value: any;
isSocialLink?: boolean;
};
const formatSocialLinks = (link: string | null): string =>
link ? '@' + link.replace('@', '') : '';
export const TableCell: FC = ({ value, isSocialLink = false }) => {
return {isSocialLink ? formatSocialLinks(value) : value} | ;
};
================================================
FILE: src/modules/TeamsList/components/Teams/components/TeamUserTable/components/TableRow/components/index.ts
================================================
export { TableCell } from './TableCell';
export { ExpelButton } from './ExpelButton';
================================================
FILE: src/modules/TeamsList/components/Teams/components/TeamUserTable/components/TableRow/index.tsx
================================================
import React, { FC } from 'react';
import { User } from 'types';
import { TableCell } from './components';
import { ExpelButton } from './components';
import { useDispatch } from 'react-redux';
import { activeModalExpel, setTeamMemberExpelId } from 'modules/TeamsList/teamsListReducer';
type TableRowProps = {
member: User;
count: number;
isMyTeam?: boolean;
userId?: string;
secondTable?: boolean;
};
export const TableRow: FC = ({
member,
count,
isMyTeam,
userId,
secondTable = false,
}) => {
const { firstName, lastName, score, telegram, discord, github, country, city, id } = member;
const dispatch = useDispatch();
return (
{isMyTeam && (
{
dispatch(activeModalExpel(true));
dispatch(setTeamMemberExpelId(id));
}}
/>
)
}
/>
)}
);
};
================================================
FILE: src/modules/TeamsList/components/Teams/components/TeamUserTable/index.tsx
================================================
import React, { FC } from 'react';
import { StyledTeamUserTable } from './styled';
import { User } from 'types';
import { TableRow } from './components/TableRow';
import { TABLE_TEAMS_HEADERS } from 'appConstants';
import { useTranslation } from 'react-i18next';
type TeamUserTableProps = {
members?: User[];
isMyTeam?: boolean;
userId?: string;
secondTable?: boolean;
};
export const TeamUserTable: FC = ({
members,
isMyTeam,
userId,
secondTable = false,
}) => {
const { t } = useTranslation();
return (
{TABLE_TEAMS_HEADERS.map((columnName: string) => {
return !isMyTeam && columnName === 'Action' ? null : (
| {t(columnName)} |
);
})}
{members &&
members.map((member: User, index: number) => {
return (
);
})}
);
};
================================================
FILE: src/modules/TeamsList/components/Teams/components/TeamUserTable/styled.tsx
================================================
import styled, { css } from 'styled-components';
import {
DASHBOARD_HEADER_BG_COLOR,
LIGHT_TEXT_COLOR,
DARK_TEXT_COLOR,
BG_COLOR,
} from 'appConstants/colors';
import { TextBold, GeneralAdaptiveFont } from 'typography';
const AdaptiveFirstCellBorderRadius = css`
border-bottom-left-radius: 10px;
border-top-left-radius: 10px;
`;
const AdaptiveLastCellBorderRadius = css`
border-bottom-right-radius: 10px;
border-top-right-radius: 10px;
`;
const AdaptiveCellVisible = css`
&:first-child {
${AdaptiveFirstCellBorderRadius}
}
&:last-child {
${AdaptiveLastCellBorderRadius}
}
@media (max-width: 992px) {
&:nth-child(5),
&:nth-child(6) {
display: none;
}
}
@media (max-width: 650px) {
&:nth-child(7) {
display: none;
}
}
@media (max-width: 500px) {
&:nth-child(3) {
display: none;
}
}
`;
export const StyledTeamUserTable = styled.table`
width: 100%;
border-collapse: collapse;
transition: all 0.3s ease-in-out;
${GeneralAdaptiveFont}
thead {
background-color: ${DASHBOARD_HEADER_BG_COLOR};
color: ${LIGHT_TEXT_COLOR};
& .SecondTable th:nth-child(4) {
@media (max-width: 650px) {
${AdaptiveLastCellBorderRadius}
}
}
& .FirstTable th:nth-child(4) {
@media (max-width: 440px) {
display: none;
}
}
th {
padding: 10px;
text-align: left;
${AdaptiveCellVisible}
}
}
tr.FirstTable td:nth-child(4) {
@media (max-width: 440px) {
display: none;
}
}
tr.SecondTable td:nth-child(4) {
@media (max-width: 650px) {
${AdaptiveLastCellBorderRadius}
}
}
tr:nth-child(even) {
background-color: ${BG_COLOR};
}
td {
color: ${DARK_TEXT_COLOR};
padding: 10px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
max-width: 150px;
${AdaptiveCellVisible}
&:nth-child(2) {
${TextBold};
font-weight: bold;
${GeneralAdaptiveFont}
}
}
`;
================================================
FILE: src/modules/TeamsList/components/Teams/components/TeamsHeader/index.tsx
================================================
import React, { FC } from 'react';
import {
TeamsHeaderStyled,
TeamsHeaderRightStyled,
TeamsHeaderSubtitleStyled,
TeamsHeaderButtonsBlockStyled,
TeamHeaderLeftStyled,
HeaderManPic,
TeamHeaderTitle,
} from './styled';
import { TeamButton } from 'typography';
import { DARK_TEXT_COLOR, WHITE_COLOR } from 'appConstants/colors';
import { useDispatch } from 'react-redux';
import { useTranslation } from 'react-i18next';
import {
activeModalJoin,
activeModalCreateTeam,
activeModalRemoveCourse,
} from 'modules/TeamsList/teamsListReducer';
export const TeamsHeader: FC = () => {
const dispatch = useDispatch();
const { t } = useTranslation();
const buttonsInfo: {
name: string;
callback: () => void;
className: string;
}[] = [
{
name: 'Create team',
callback: () => dispatch(activeModalCreateTeam(true)),
className: 'secondStep',
},
{
name: 'Join team',
callback: () => dispatch(activeModalJoin(true)),
className: 'thirdStep',
},
{
name: 'Leave course',
callback: () => dispatch(activeModalRemoveCourse(true)),
className: 'fourthStep',
},
];
return (
{t('Become a member of the team!')}
{t('To become a member')}
{buttonsInfo.map((item) => {
return (
{t(item.name)}
);
})}
);
};
================================================
FILE: src/modules/TeamsList/components/Teams/components/TeamsHeader/styled.ts
================================================
import styled from 'styled-components';
import { WHITE_COLOR, MAIN2_COLOR } from 'appConstants/colors';
import { ReactComponent as HeaderMan } from 'assets/svg/teams-man.svg';
import { PageSubTitle, TextRegular, H2AdaptiveFont, GeneralAdaptiveFont } from 'typography';
export const TeamsHeaderStyled = styled.div`
padding: 30px;
position: relative;
color: ${WHITE_COLOR};
background-color: ${MAIN2_COLOR};
border-radius: 20px;
z-index: 0;
margin-bottom: 40px;
@media (max-width: 580px) {
overflow: hidden;
}
`;
export const TeamHeaderTitle = styled.h2`
${PageSubTitle};
${H2AdaptiveFont};
margin: 5px 0 18px;
`;
export const TeamsHeaderRightStyled = styled.div`
max-width: 631px;
width: 100%;
`;
export const TeamsHeaderSubtitleStyled = styled.div`
${TextRegular};
${GeneralAdaptiveFont};
color: ${WHITE_COLOR};
margin-bottom: 30px;
`;
export const TeamsHeaderButtonsBlockStyled = styled.div`
display: flex;
gap: 20px;
@media (max-width: 680px) {
gap: 10px;
}
@media (max-width: 580px) {
flex-direction: column;
align-content: center;
justify-content: center;
}
`;
export const TeamHeaderLeftStyled = styled.div`
position: absolute;
right: 0;
top: 0;
bottom: 0;
max-width: 631px;
width: 100%;
z-index: -1;
`;
export const HeaderManPic = styled(HeaderMan)`
position: absolute;
right: 40px;
bottom: 0;
width: 440px;
height: 254px;
z-index: -1;
`;
================================================
FILE: src/modules/TeamsList/components/Teams/components/index.ts
================================================
export { TeamsHeader } from './TeamsHeader';
export { MyTeam } from './MyTeam';
export { TeamItem } from './TeamItem';
================================================
FILE: src/modules/TeamsList/components/Teams/index.tsx
================================================
import React, { FC } from 'react';
import { TeamsHeader, MyTeam, TeamItem } from './components';
import { Team, TeamList } from 'types';
import { TableTitle } from 'modules/StudentsTable/styled';
import { TeamsTitleWrapper } from 'modules/TeamsList/styled';
import { useTranslation } from 'react-i18next';
type TeamsProps = {
teams: TeamList;
myTeam?: Team;
userId: string;
};
export const Teams: FC = ({ teams, myTeam, userId }) => {
const { t } = useTranslation();
return (
<>
{t('Teams')}
{myTeam ? : }
{!!teams.count &&
teams.results.map((team) => (
))}
>
);
};
================================================
FILE: src/modules/TeamsList/components/index.ts
================================================
export { Teams } from './Teams';
export { TeamListModals } from './TeamListModals';
================================================
FILE: src/modules/TeamsList/index.tsx
================================================
import React, { FC, useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { useTeamsQuery } from 'hooks/graphql';
import { Loader, ErrorModal, Pagination } from 'components';
import { selectUserData } from 'modules/StudentsTable/selectors';
import { selectCurrCourse } from 'modules/LoginPage/selectors';
import { StyledTeams } from './styled';
import { TEAMS_PER_PAGE, TOUR_OPENING } from 'appConstants';
import { Team } from 'types';
import { TeamListModals, Teams } from './components';
import { useCommonMutations } from './components/TeamListModals/useCommonMutations';
import { AdditionalWrapper, ContentPageWrapper } from 'typography';
import { setIsTourOpen } from 'modules/LoginPage/loginPageReducer';
import { setTourOpening } from 'modules/LoginPage/loginPageMiddleware';
export const TeamsList: FC = () => {
const [page, setPage] = useState(0);
const currCourse = useSelector(selectCurrCourse);
const userData = useSelector(selectUserData);
const dispatch = useDispatch();
const userTeam = userData.teams?.find((team: Team) => team.courseId === currCourse.id);
const { loadingT, errorT, teams } = useTeamsQuery({
reactCourseId: currCourse.id,
page,
});
const loading = loadingT;
const error = errorT;
const {
addUserToTeam,
removeUserFromTeam,
expelUserFromTeam,
createTeam,
updateTeam,
removeUserFromCourse,
commonMutationError,
isLoading,
} = useCommonMutations(page);
if (!localStorage.getItem(TOUR_OPENING) && !userTeam) {
dispatch(setIsTourOpen(true));
}
dispatch(setTourOpening(TOUR_OPENING));
if (error || commonMutationError) return ;
if (loading || isLoading) return ;
const pageCount: number = Math.ceil(teams.count / TEAMS_PER_PAGE);
return (
{!!teams.results.length && (
)}
);
};
================================================
FILE: src/modules/TeamsList/selectors.ts
================================================
import { State } from 'types';
export const selectIsActiveModalExpel = (state: State) => state.teamsListReducer.isActiveModalExpel;
export const selectIsActiveModalLeave = (state: State) => state.teamsListReducer.isActiveModalLeave;
export const selectIsActiveModalJoin = (state: State) => state.teamsListReducer.isActiveModalJoin;
export const selectIsActiveModalCreateTeam = (state: State) =>
state.teamsListReducer.isActiveModalCreateTeam;
export const selectIsActiveModalCreated = (state: State) =>
state.teamsListReducer.isActiveModalCreated;
export const selectIsActiveModalRemoveCourse = (state: State) =>
state.teamsListReducer.isActiveModalRemoveCourse;
export const selectIsActiveModalUpdateSocialLink = (state: State) =>
state.teamsListReducer.isActiveModalUpdateSocialLink;
export const selectIsActiveModalSortStudents = (state: State) =>
state.teamsListReducer.isActiveModalSortStudents;
export const selectIsActiveModalLeavePage = (state: State) =>
state.teamsListReducer.isActiveModalLeavePage;
export const selectIsActiveModalCreatedCourse = (state: State) =>
state.teamsListReducer.isActiveModalCreatedCourse;
export const selectIsActiveModalEditCourse = (state: State) =>
state.teamsListReducer.isActiveModalEditCourse;
export const selectTeamMemberExpelId = (state: State) => state.teamsListReducer.teamMemberExpelId;
export const selectTeamPassword = (state: State) => state.teamsListReducer.teamPassword;
export const selectSocialLink = (state: State) => state.teamsListReducer.socialLink;
================================================
FILE: src/modules/TeamsList/styled.ts
================================================
import styled from 'styled-components';
export const StyledTeams = styled.div`
max-width: 1320px;
width: 92%;
`;
export const TeamsTitleWrapper = styled.div`
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
h1 {
width: auto;
}
`;
================================================
FILE: src/modules/TeamsList/teamsListReducer.ts
================================================
import {
SET_SOCIAL_LINK,
SET_TEAM_PASSWORD,
SET_TEAM_MEMBER_EXPEL_ID,
ACTIVE_MODAL_EXPEL,
ACTIVE_MODAL_LEAVE,
ACTIVE_MODAL_JOIN,
ACTIVE_MODAL_CREATE_TEAM,
ACTIVE_MODAL_CREATED,
ACTIVE_MODAL_UPDATE_SOCIAL_LINK,
ACTIVE_MODAL_REMOVE_COURSE,
ACTIVE_MODAL_SORT_STUDENTS,
ACTIVE_MODAL_LEAVE_PAGE,
ACTIVE_MODAL_CREATED_COURSE,
ACTIVE_MODAL_EDIT_COURSE,
} from 'appConstants';
import { StateTeamsList } from 'types';
import { createActions, handleActions } from 'redux-actions';
export const teamsListState = {
socialLink: '',
teamPassword: '',
teamMemberExpelId: '',
isActiveModalExpel: false,
isActiveModalLeave: false,
isActiveModalJoin: false,
isActiveModalCreateTeam: false,
isActiveModalCreated: false,
isActiveModalUpdateSocialLink: false,
isActiveModalRemoveCourse: false,
isActiveModalSortStudents: false,
isActiveModalLeavePage: false,
isActiveModalCreatedCourse: false,
isActiveModalEditCourse: false,
};
export const {
setSocialLink,
setTeamPassword,
setTeamMemberExpelId,
activeModalExpel,
activeModalLeave,
activeModalJoin,
activeModalCreateTeam,
activeModalCreated,
activeModalUpdateSocialLink,
activeModalRemoveCourse,
activeModalSortStudents,
activeModalLeavePage,
activeModalCreatedCourse,
activeModalEditCourse,
} = createActions({
SET_SOCIAL_LINK: (socialLink) => ({ socialLink }),
SET_TEAM_PASSWORD: (teamPassword) => ({ teamPassword }),
SET_TEAM_MEMBER_EXPEL_ID: (teamMemberExpelId) => ({ teamMemberExpelId }),
ACTIVE_MODAL_EXPEL: (isActiveModalExpel) => ({ isActiveModalExpel }),
ACTIVE_MODAL_LEAVE: (isActiveModalLeave) => ({ isActiveModalLeave }),
ACTIVE_MODAL_JOIN: (isActiveModalJoin) => ({
isActiveModalJoin,
}),
ACTIVE_MODAL_CREATE_TEAM: (isActiveModalCreateTeam) => ({
isActiveModalCreateTeam,
}),
ACTIVE_MODAL_CREATED: (isActiveModalCreated) => ({ isActiveModalCreated }),
ACTIVE_MODAL_UPDATE_SOCIAL_LINK: (isActiveModalUpdateSocialLink) => ({
isActiveModalUpdateSocialLink,
}),
ACTIVE_MODAL_REMOVE_COURSE: (isActiveModalRemoveCourse) => ({
isActiveModalRemoveCourse,
}),
ACTIVE_MODAL_SORT_STUDENTS: (isActiveModalSortStudents) => ({
isActiveModalSortStudents,
}),
ACTIVE_MODAL_LEAVE_PAGE: (isActiveModalLeavePage) => ({
isActiveModalLeavePage,
}),
ACTIVE_MODAL_CREATED_COURSE: (isActiveModalCreatedCourse) => ({
isActiveModalCreatedCourse,
}),
ACTIVE_MODAL_EDIT_COURSE: (isActiveModalEditCourse) => ({
isActiveModalEditCourse,
}),
});
export const teamsListReducer = handleActions(
{
[SET_SOCIAL_LINK]: (state, { payload: { socialLink } }) => ({
...state,
socialLink,
}),
[SET_TEAM_PASSWORD]: (state, { payload: { teamPassword } }) => ({
...state,
teamPassword,
}),
[SET_TEAM_MEMBER_EXPEL_ID]: (state, { payload: { teamMemberExpelId } }) => ({
...state,
teamMemberExpelId,
}),
[ACTIVE_MODAL_EXPEL]: (state, { payload: { isActiveModalExpel } }) => ({
...state,
isActiveModalExpel,
}),
[ACTIVE_MODAL_LEAVE]: (state, { payload: { isActiveModalLeave } }) => ({
...state,
isActiveModalLeave,
}),
[ACTIVE_MODAL_JOIN]: (state, { payload: { isActiveModalJoin } }) => ({
...state,
isActiveModalJoin,
}),
[ACTIVE_MODAL_CREATE_TEAM]: (state, { payload: { isActiveModalCreateTeam } }) => ({
...state,
isActiveModalCreateTeam,
}),
[ACTIVE_MODAL_CREATED]: (state, { payload: { isActiveModalCreated } }) => ({
...state,
isActiveModalCreated,
}),
[ACTIVE_MODAL_UPDATE_SOCIAL_LINK]: (state, { payload: { isActiveModalUpdateSocialLink } }) => ({
...state,
isActiveModalUpdateSocialLink,
}),
[ACTIVE_MODAL_REMOVE_COURSE]: (state, { payload: { isActiveModalRemoveCourse } }) => ({
...state,
isActiveModalRemoveCourse,
}),
[ACTIVE_MODAL_SORT_STUDENTS]: (state, { payload: { isActiveModalSortStudents } }) => ({
...state,
isActiveModalSortStudents,
}),
[ACTIVE_MODAL_LEAVE_PAGE]: (state, { payload: { isActiveModalLeavePage } }) => ({
...state,
isActiveModalLeavePage,
}),
[ACTIVE_MODAL_CREATED_COURSE]: (state, { payload: { isActiveModalCreatedCourse } }) => ({
...state,
isActiveModalCreatedCourse,
}),
[ACTIVE_MODAL_EDIT_COURSE]: (state, { payload: { isActiveModalEditCourse } }) => ({
...state,
isActiveModalEditCourse,
}),
},
teamsListState
);
================================================
FILE: src/modules/TokenPage/index.tsx
================================================
import React, { FC, useEffect } from 'react';
import { AUTH_TOKEN } from 'appConstants';
import { useDispatch } from 'react-redux';
import { useParams, useHistory } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { setToken } from 'modules/LoginPage/loginPageReducer';
type ParamsType = {
id: string;
};
export const TokenPage: FC = () => {
const dispatch = useDispatch();
const { id } = useParams();
const history = useHistory();
const { t } = useTranslation();
useEffect(() => {
const loginToken = sessionStorage.getItem(AUTH_TOKEN);
if (!loginToken) {
sessionStorage.setItem(AUTH_TOKEN, id);
dispatch(setToken(id));
}
history.push('/');
}, [id, history, dispatch]);
return (
);
};
================================================
FILE: src/modules/TutorialPage/components/NoteBlock/index.tsx
================================================
import React, { FC } from 'react';
import { NoteWrapper, NoteList } from './styled';
import { useTranslation } from 'react-i18next';
type NoteBlockProps = {
title?: string;
subtitle?: string;
listItems?: string[];
isNote?: string;
};
export const NoteBlock: FC = ({
title = 'Note',
subtitle,
listItems,
children,
isNote,
}) => {
const { t } = useTranslation();
return (
{t(title)}
{subtitle && {t(subtitle)}
}
{listItems && (
{listItems.map((item: string) => (
{t(item)}
))}
)}
{children}
);
};
================================================
FILE: src/modules/TutorialPage/components/NoteBlock/styled.ts
================================================
import {
DARK_TEXT_COLOR,
MAIN1_COLOR,
DASHBOARD_HEADER_BG_COLOR,
WHITE_COLOR,
} from 'appConstants/colors';
import styled from 'styled-components';
type NoteProps = {
isNote?: string;
};
export const NoteWrapper = styled.div`
display: flex;
flex-direction: column;
width: 100%;
margin-bottom: ${({ isNote }) => isNote && '20px'};
padding: ${({ isNote }) => isNote && '30px'};
background-color: ${({ isNote }) => isNote && DASHBOARD_HEADER_BG_COLOR};
border-radius: 20px;
p {
margin-top: 0;
}
& > h4 {
color: ${({ isNote }) => (isNote ? MAIN1_COLOR : DARK_TEXT_COLOR)};
margin: 0 0 20px;
}
.SelectCourseExample {
width: 300px;
margin-left: 40px;
@media (max-width: 550px) {
width: 250px;
margin-left: 35px;
}
@media (max-width: 440px) {
width: 200px;
margin-left: 30px;
}
}
@media (max-width: 650px) {
padding: ${({ isNote }) => isNote && '27.5px'};
}
@media (max-width: 550px) {
padding: ${({ isNote }) => isNote && '25px'};
}
@media (max-width: 440px) {
padding: ${({ isNote }) => isNote && '20px'};
margin-bottom: ${({ isNote }) => isNote && '10px'};
}
`;
export const NoteList = styled.ol`
display: flex;
flex-direction: column;
gap: 25px;
list-style: none;
counter-reset: counter;
margin: 0 0 20px;
padding-inline-start: 0;
li {
counter-increment: counter;
margin: 0 0 0 40px;
text-align: justify;
@media (max-width: 550px) {
margin-left: 35px;
}
@media (max-width: 440px) {
margin-left: 30px;
}
}
li::before {
content: counter(counter);
background-color: ${MAIN1_COLOR};
width: 30px;
height: 30px;
border-radius: 50%;
display: inline-block;
line-height: 30px;
color: ${WHITE_COLOR};
text-align: center;
margin: 0 10px -3px -40px;
@media (max-width: 550px) {
width: 25px;
height: 25px;
line-height: 25px;
margin-left: -35px;
}
@media (max-width: 440px) {
width: 20px;
height: 20px;
line-height: 22px;
margin-left: -30px;
}
}
@media (max-width: 550px) {
gap: 20px;
}
@media (max-width: 440px) {
gap: 15px;
}
`;
================================================
FILE: src/modules/TutorialPage/components/StepBlock/index.tsx
================================================
import React, { FC } from 'react';
import { useTranslation } from 'react-i18next';
import { StepBlockWrapper, StepBlockText } from './styled';
import { PageTitle } from 'typography';
type StepBlockProps = {
title: string;
subtitle: string;
imageSrc: string;
altText: string;
};
export const StepBlock: FC = ({ title, subtitle, imageSrc, altText }) => {
const { t } = useTranslation();
return (
{t(title)}
{t(subtitle)}
);
};
================================================
FILE: src/modules/TutorialPage/components/StepBlock/styled.ts
================================================
import styled from 'styled-components';
import { TextRegular } from 'typography';
export const StepBlockWrapper = styled.div`
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
& > * {
margin-bottom: 20px;
}
`;
export const StepBlockText = styled.p`
${TextRegular};
margin-top: 0;
`;
================================================
FILE: src/modules/TutorialPage/components/index.ts
================================================
export { StepBlock } from './StepBlock';
export { NoteBlock } from './NoteBlock';
================================================
FILE: src/modules/TutorialPage/index.tsx
================================================
import React, { FC, useCallback } from 'react';
import { TutorialPageWrapper } from './styled';
import editProfileExampleEN from 'assets/images/editProfileExampleEN.png';
import editProfileExampleRU from 'assets/images/editProfileExampleRU.png';
import teamActionsExampleEN from 'assets/images/teamActionsExampleEN.png';
import teamActionsExampleRU from 'assets/images/teamActionsExampleRU.png';
import { NoteBlock, StepBlock } from './components';
import { tutorialNoteInfo } from './tutorialPageInfo';
import { useSelector } from 'react-redux';
import { selectCurrLanguage } from 'modules/LoginPage/selectors';
import { ContentPageWrapper } from 'typography';
import { useTranslation } from 'react-i18next';
export const TutorialPage: FC = () => {
const currentLang = useSelector(selectCurrLanguage);
const { t } = useTranslation();
const TUTORIAL_PAGE_NOTES_INFO = useCallback(
() => tutorialNoteInfo(currentLang, t),
[currentLang, t]
);
return (
{TUTORIAL_PAGE_NOTES_INFO().map(({ title, subtitle, listItems, isNote, children }) => (
{children}
))}
);
};
================================================
FILE: src/modules/TutorialPage/styled.ts
================================================
import styled from 'styled-components';
import { GeneralAdaptiveFont } from 'typography';
export const TutorialPageWrapper = styled.div`
display: flex;
flex-direction: column;
width: 680px;
height: fit-content;
padding: 60px 0;
gap: 40px;
.StepBlockImage {
margin: 20px 0;
border-radius: 20px;
box-shadow: 0px 4px 50px rgba(6, 73, 140, 0.1);
@media (max-width: 440px) {
margin: 0;
}
}
div:nth-of-type(6) {
margin-top: -17px;
}
div:last-child {
margin-top: 20px;
& > p {
margin-bottom: 0;
}
}
p,
li {
${GeneralAdaptiveFont};
}
h4 {
font-size: 20px;
line-height: 30px;
}
@media (max-width: 992px) {
width: 640px;
h4 {
font-size: 1.3rem;
}
}
@media (max-width: 768px) {
width: 600px;
h1 {
font-size: 1.3rem;
}
h4 {
font-size: 1.2rem;
}
}
@media (max-width: 650px) {
width: 500px;
padding: 55px 0;
gap: 30px;
div:last-child {
margin-top: 0;
}
h1 {
font-size: 1.25rem;
}
h4 {
font-size: 1.15rem;
}
}
@media (max-width: 550px) {
width: 400px;
padding: 50px 0;
gap: 25px;
h1 {
font-size: 1.1rem;
}
h4 {
font-size: 1rem;
}
}
@media (max-width: 440px) {
width: 280px;
padding: 40px 0;
gap: 20px;
h1 {
font-size: 1rem;
}
h4 {
font-size: 0.9rem;
line-height: 22px;
}
}
`;
================================================
FILE: src/modules/TutorialPage/tutorialPageInfo.tsx
================================================
import myTeamExampleEN from 'assets/images/myTeamExampleEN.png';
import myTeamExampleRU from 'assets/images/myTeamExampleRU.png';
export const tutorialNoteInfo = (currentLang: string, t: any) => {
const TUTORIAL_PAGE_NOTES_INFO = [
{
title: 'Create team',
subtitle: 'The team creation process is taken place in three steps',
listItems: [
'Press the “Create team” button',
'Provide a link to a team chat',
'Specify password',
],
children: {t('The only person who creates the team is Team Lead')}
,
},
{
title: 'Join team',
subtitle: 'To join an existing team',
listItems: [`Press the “Join team” button`, `Specify the password`],
},
{
listItems: [
`If you have not found a team`,
`If the team has fewer members than is required for the task`,
],
isNote: 'Note',
},
{
title: 'You will see the following after joining the team',
children: (
),
},
{
title: 'Leave team',
subtitle: 'To leave the team',
},
];
return TUTORIAL_PAGE_NOTES_INFO;
};
================================================
FILE: src/modules/index.ts
================================================
export { LoginPage } from './LoginPage';
export { StudentsTable } from './StudentsTable';
export { TeamsList } from './TeamsList';
export { TokenPage } from './TokenPage';
export { NotFoundPage } from './NotFoundPage';
export { EditProfile } from './EditProfile';
export { TutorialPage } from './TutorialPage';
export { AdminPage } from './AdminPage';
================================================
FILE: src/react-app-env.d.ts
================================================
///
declare module '*.woff2';
declare module '*.svg' {
import React = require('react');
export const ReactComponent: React.SFC>;
const src: string;
export default src;
}
declare module '*.jpg' {
const content: string;
export default content;
}
declare module '*.png' {
const content: string;
export default content;
}
================================================
FILE: src/reportWebVitals.ts
================================================
import { ReportHandler } from 'web-vitals';
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;
================================================
FILE: src/setupTests.ts
================================================
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
================================================
FILE: src/store/index.tsx
================================================
import React, { FC } from 'react';
import { Provider } from 'react-redux';
import { studentsTableReducer } from 'modules/StudentsTable/studentsTableReducer';
import { teamsListReducer } from 'modules/TeamsList/teamsListReducer';
import { loginPageReducer } from 'modules/LoginPage/loginPageReducer';
import { createStore, combineReducers, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension/developmentOnly';
import thunkMiddleware from 'redux-thunk';
import { State } from 'types';
const appReducer = combineReducers({
studentsTableReducer,
teamsListReducer,
loginPageReducer,
});
const store = createStore(appReducer, composeWithDevTools(applyMiddleware(thunkMiddleware)));
export const AppState: FC = ({ children }) => {
return (
<>{children}>
);
};
================================================
FILE: src/translation/en/en.json
================================================
{
"First / Last Name": "First / Last Name",
"Score": "Score",
"Team Number": "Team Number",
"Location": "Location",
"Courses": "Courses",
"Course": "Course",
"Action": "Action",
"Use the format": "Use right format link - https://xxx/xxxx",
"This input exceed maxLength.": "This input exceed maxLength.",
"Development" : "Development:",
"Design": "Design:",
"Clear filter": "Clear filter",
"Apply": "Apply",
"Please, enter link": "Please, enter link",
"Enter group link": "Enter group link",
"Enter team password": "Enter team password",
"Enter your profile information": "Enter your profile information",
"Select course": "Select course",
"Submit": "Submit",
"Save": "Save",
"First Name": "First Name",
"Enter first name": "Enter first name",
"Last Name": "Last Name",
"This is required.": "This is required.",
"Minimal length is 1.": "Minimal length is 1.",
"Minimal length is 2.": "Minimal length is 2.",
"Minimal length is 3.": "Minimal length is 3.",
"Minimal length is 5.": "Minimal length is 5.",
"Enter last name": "Enter last name",
"Enter telegram": "Enter telegram",
"Enter discord": "Enter discord",
"This input is letters only.": "This input is letters only.",
"This input is number only.": "This input is number only.",
"This input is letters and digits only.": "This input is letters and digits only.",
"City": "City",
"Enter city": "Enter city",
"Country": "Country",
"Enter country": "Enter country",
"Enter score": "Enter score",
"Sign in": "Sign in",
"Sign in with Github": "Sign in with Github",
"Don’t have github account?": "Don’t have GitHub account?",
"Sign up": "Sign up",
"Not found!": "Page not found! :(",
"Dashboard": "Dashboard",
"Filter": "Filter",
"No team yet.": "No team yet.",
"No": "No",
"No courses.": "No courses.",
"Redirecting...": "Redirecting...",
"Teams": "Teams",
"Team": "Team",
"Sort students": "Sort students",
"Sort students?": "Sort students?",
"member": "member",
"members": "members",
"My team - Team": "My team - Team",
"Leave team": "Leave team",
"Join team": "Join team",
"The password is required to join the team.": "The password is required to join the team.",
"Become a member of the team!": "Become a member of the team!",
"To become a member": "To become a member of the team you can create your own team or join team. If not, you will be added to the team automatically.",
"Create team": "Create team",
"Expel": "Expel",
"Edit Profile": "Edit Profile",
"Something went wrong!": "Something went wrong!",
"Please, try again later.": "Please, try again later. If problem is not solved - contact us in telegram:",
"Ok": "Ok",
"Please, enter your team password.": "Please, enter your team password.",
"Wrong password!": "Wrong password! Please, also check if the team you want to join applies to the current course in your profile.",
"Are you sure want to leave team?": "Are you sure want to leave team?",
"Yes": "Yes",
"Expel User": "Expel User",
"Are you sure want to expel user?": "Are you sure want to expel user?",
"Leave course": "Leave course",
"Are you sure to leave this course?": "Are you sure to leave this course?",
"Please, enter your team telegram / discord / viber / ets. group link.": "Please, enter your team telegram / discord / viber / ets. group link.",
"New team created!": "New team created!",
"You are automatically added there.": "You are automatically added there.",
"If you want to invite friends - tell them your team password:": "If you want to invite friends - tell them your team password:",
"Got it!": "Got it!",
"Link to group": "Link to group",
"Please, enter new group link.": "Please, enter new group link.",
"Update link": "Update link",
"Sort by score": "Sort by score",
"Max score": "Max score",
"Min score": "Min score",
"Sort by team": "Sort by team",
"All": "All",
"Without team": "Without team",
"Enter location": "Enter location",
"Enter github name": "Enter GitHub name",
"Enter discord name": "Enter discord name",
"Enter course name": "Enter course name",
"Cancel": "Cancel",
"Invitation password": "Invitation password",
"You need to choose at least one course": "You need to choose at least one course",
"Step 1": "Step 1 - Registration",
"The app will suggest": "The app will suggest you to fill in the registration form after authorization:",
"The “score” field should be filled in with your score": "The “score” field should be filled in with your score for the base course at RS School (Front-end/JavaScript). If you have not studied in the base course, fill in this field with 100.",
"Step 2": "Step 2 - Team creation or team joining",
"On the teams page you will see two available buttons": "On the teams page you will see two available buttons - “Join team” and “Create team”:",
"Note": "Note",
"The team creation process is taken place in three steps": "The team creation process is taken place in three steps:",
"Press the “Create team” button": "Press the “Create team” button;",
"Provide a link to a team chat": "Provide a link to a team chat (viber, discord, telegram);",
"Specify password": "Specify password (another teammates can join this team later using this password).",
"The only person who creates the team is Team Lead": "The only person who creates the team is Team Lead and then he or she shares the team password to another team’s participants.",
"To join an existing team": "To join an existing team you need to know the team password and go through the following steps:",
"Press the “Join team” button": "Press the “Join team” button;",
"Specify the password": "Specify the password which you have received from the team creator.",
"If you have not found a team": "If you have not found a team, you should just register in the app and we will find a team for you when registration closes.",
"If the team has fewer members than is required for the task": "If the team has fewer members than is required for the task, then it will be automatically staffed to the required number when registration closes.",
"You will see the following after joining the team": "You will see the following after joining the team:",
"To leave the team": "To leave the team, click the “Leave Team” button or ask someone from your teammates to exclude you from the team.",
"Data is unsaved": "Data is unsaved",
"Are you sure want to leave page without saving data": "Are you sure want to leave page without saving data?",
"Welcome to RSS Teams": "Welcome to RSS Teams!",
"Skip": "Skip",
"Next": "Next",
"You can create new team": "You can create new team - you need only the link to your future team chat.",
"Or join existing team": "Or join existing team! You should have the password from your team.",
"You can always leave the course": "You can always leave the course.",
"On Teams page you can see other teams": "On Teams page you can also see other teams.",
"With dropdown you can switch the course": "With dropdown you can switch the course in case if you have several courses.",
"On Dashboard page": "On Dashboard page you can check/find another students.",
"With filter usage": "With filter usage you can make the search for students easier.",
"On Edit profile page": "On Edit profile page you can add/remove course or edit your profile.",
"If you forget something": "If you forgot something, you can use Tutorial tab.",
"Support us": "Support us by pressing a star in",
"our repo": "our repo",
"Got it": "Got it!",
"Admin": "Admin",
"Admin page": "Admin page",
"Course name": "Course name",
"Team size": "Team size",
"Enter team size": "Enter team size",
"Add new course": "Add new course",
"Courses list": "Courses list",
"Show": "Show",
"Search course": "Search course...",
"Active(status)": "Active",
"Active": "Active",
"Terminate": "Terminate",
"Terminate(status)": "Terminated",
"Terminated": "Terminated",
"Edit course": "Edit course",
"Course was created": "Course was created!",
"If you want to change something in new course": "If you want to change something in new course you can edit it below",
"New course name": "New course name",
"New team size": "New team size",
"Enter new course name": "Enter new course name",
"Enter new team size": "Enter new team size",
"Please, enter new course information": "Please, enter new course information",
"Please, enter unique course name": "Please, enter unique course name",
"Please, enter correct course name": "Should include current year",
"Please, enter correct team size": "Please, enter correct team size (2-9)",
"Terminate course": "Terminate course",
"Activate course": "Activate course",
"Close": "Close",
"Unset": "Unset"
}
================================================
FILE: src/translation/resources.ts
================================================
import { CURRENT_LANG, DEFAULT_LANGUAGE } from 'appConstants';
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import translationEN from './en/en.json';
import translationRU from './ru/ru.json';
const resources = {
en: {
translation: translationEN,
},
ru: {
translation: translationRU,
},
};
const language = localStorage.getItem(CURRENT_LANG) ?? DEFAULT_LANGUAGE;
i18n.use(initReactI18next).init({
resources,
lng: language,
keySeparator: false,
interpolation: {
escapeValue: false,
},
});
export default i18n;
================================================
FILE: src/translation/ru/ru.json
================================================
{
"First / Last Name": "Имя и фамилия",
"Score": "Рейтинг",
"Team Number": "№ команды",
"Location": "Локация",
"Courses": "Курсы",
"Course": "Курс",
"Action": "Действие",
"Use the format": "Используйте верный формат ссылки - https://xxx/xxxx",
"This input exceed maxLength.": "Превышена максимальная длина ввода!",
"Development" : "Разработка:",
"Design": "Дизайн:",
"Clear filter": "Очистить фильтр",
"Apply": "Применить",
"Please, enter link": "Пожалуйста, введите ссылку",
"Enter group link": "Введите ссылку группы",
"Enter team password": "Введите пароль команды",
"Enter your profile information": "Заполните информацию Вашего профиля",
"Select course": "Выберите курс",
"Submit": "Отправить",
"Save": "Сохранить",
"First Name": "Имя",
"Enter first name": "Введите имя",
"Last Name": "Фамилия",
"This is required.": "Это поле не может быть пустым.",
"Minimal length is 1.": "Минимальная длина 1.",
"Minimal length is 2.": "Минимальная длина 2.",
"Minimal length is 3.": "Минимальная длина 3.",
"Minimal length is 5.": "Минимальная длина 5.",
"Enter last name": "Введите фамилию",
"Enter telegram": "Введите telegram",
"Enter discord": "Введите discord",
"This input is letters only.": "Вы можете ввести только буквы.",
"This input is number only.": "Вы можете ввести только цифры.",
"This input is letters and digits only.": "Вы можете ввести только цифры или буквы.",
"City": "Город",
"Enter city": "Введите город",
"Country": "Страна",
"Enter country": "Введите страну",
"Enter score": "Введите рейтинг",
"Sign in": "Войти",
"Sign in with Github": "Войти через Github",
"Don’t have github account?": "У Вас нет аккаунта Github?",
"Sign up": "Зарегистрироваться",
"Not found!": "Не найдено!",
"Dashboard": "Таблица",
"Filter": "Фильтр",
"No team yet.": "Нет команды.",
"No": "Нет",
"No courses.": "Нет курсов.",
"Redirecting...": "Перенаправление...",
"Teams": "Команды",
"Team": "Команда",
"Sort students": "Сортировка студентов",
"Sort students?": "Рассортировать студентов?",
"member": "участник",
"members": "участника",
"My team - Team": "Моя команда - Команда",
"Leave team": "Покинуть команду",
"Join team": "Вступить в команду",
"The password is required to join the team.": "Пароль обязателен для вступления в команду",
"Become a member of the team!": "Станьте участником команды!",
"To become a member": "Чтобы стать участником команды, Вам необходимо создать Вашу собствнную команду или вступить в уже существующую. Если нет, Вы будете автоматически добавлены в команду.",
"Create team": "Создать команду",
"Expel": "Исключить",
"Edit Profile": "Профиль",
"Something went wrong!": "Что-то пошло не так :(",
"Please, try again later.": "Пожалуйста, попробуйте позже. Если проблема не будет решена, свяжытесь с нами через telegram:",
"Ok": "Ok",
"Please, enter your team password.": "Пожалуйста, ввестите пароль команды.",
"Wrong password!": "Неверный пароль! Пожалуйста, также проверьте, относится ли Ваша команда к текущему курсу у Вас в профиле.",
"Are you sure want to leave team?": "Вы уверены, что хотите покинуть команду?",
"Yes": "Да",
"Expel User": "Исключение участника",
"Are you sure want to expel user?": "Вы уверены, что хотите исключить участника?",
"Leave course": "Покинуть курс",
"Are you sure to leave this course?": "Вы уверены, что хотите покинуть курс?",
"Please, enter your team telegram / discord / viber / ets. group link.": "Пожалуйста, введите ссылку на Ваш командный чат telegram / discord / viber / др.",
"New team created!": "Новая команда создана!",
"You are automatically added there.": "Вы автоматически добавлены туда.",
"If you want to invite friends - tell them your team password:": "Если Вы хотите пригласить друзей, - скажите им Ваш пароль от команды:",
"Got it!": "Есть!",
"Link to group": "Ссылка на группу",
"Please, enter new group link.": "Пожалуйста, введите новую ссылку группы.",
"Update link": "Обновить ссылку",
"Sort by score": "Сортировать по рейтингу",
"Max score": "Максимальный",
"Min score": "Минимальный",
"Sort by team": "Сортировать по командам",
"All": "Все",
"Without team": "Без команды",
"Enter location": "Введите локацию",
"Enter github name": "Введите имя GitHub",
"Enter discord name": "Введите имя discord",
"Enter course name": "Введите название курса",
"Cancel": "Закрыть",
"Invitation password": "Пароль команды",
"You need to choose at least one course": "Необходимо выбрать хотя бы один курс",
"Step 1": "Шаг № 1 Регистрация",
"The app will suggest": "После авторизации приложение предложит Вам заполнить регистрационную форму:",
"The “score” field should be filled in with your score": "Поле score заполняется в соответствии с Вашим скором на основном курсе в RS School. Если Вы не учились на основном курсе, то указываем 100, чтобы прошла валидация.",
"Step 2": "Шаг № 2 Создание команды или присоединение к команде",
"On the teams page you will see two available buttons": "На странице Команды Вы увидите две доступные кнопки - 'Присоединиться к команде' и 'Создать команду':",
"Note": "Примечание",
"The team creation process is taken place in three steps": "Процесс создания команды проходит в три шага:",
"Press the “Create team” button": "Нажать 'Создать команду';",
"Provide a link to a team chat": "Указать ссылку на командный чат (viber, discord, telegram);",
"Specify password": "Указать пароль (по паролю остальные члены команды смогут позже присоединиться).",
"The only person who creates the team is Team Lead": "Команду создает только один из участников (Team lead) и делится со всеми остальными участниками паролем.",
"To join an existing team": "Чтобы присоединиться к уже существующей команде вам нужно знать пароль и пройти следующие шаги:",
"Press the “Join team” button": "Нажать 'Присоединиться к команде';",
"Specify the password": "Указать пароль, которым с вами поделился создатель команды.",
"If you have not found a team": "Если Вы не нашли себе команду, то просто регистрируетесь в приложении и мы найдем команду для вас.",
"If the team has fewer members than is required for the task": "Если в команде меньше участников, чем указано в задании, команда будет укомплектована до нужного количества автоматически.",
"You will see the following after joining the team": "После присоединения к команде Вы увидите:",
"To leave the team": "Чтобы покинуть команду достаточно нажать кнопку 'Покинуть команду'. Или чтобы кто-то из сокомандников вас исключил.",
"Data is unsaved": "Данные не сохранены",
"Are you sure want to leave page without saving data": "Вы уверены, что хотите покинуть страницу без сохранения данных?",
"Welcome to RSS Teams": "Добро пожаловать в RSS Teams!",
"Skip": "Закрыть",
"Next": "Дальше!",
"You can create new team": "Вы можете создать новую команду - Вам нужна только ссылка чата Вашей группы.",
"Or join existing team": "Или Вы можете присоединиться к существующей команде! Вы должны получить пароль Вашей команды у членов Вашей команды.",
"You can always leave the course": "Вы можете покинуть курс в любое время.",
"On Teams page you can see other teams": "На странице 'Команды' Вы можете просмотреть команды других студентов.",
"With dropdown you can switch the course": "С помощью выпадающего списка Вы можете переключать текущий курс при условии, что у Вас несколько курсов.",
"On Dashboard page": "На странице 'Таблица' Вы можете найти других студентов.",
"With filter usage": "При использовании фильтров можно сделать поиск проще.",
"On Edit profile page": "На странице 'Профиль' Вы можете добавить/удалить курс или отредактировать Ваш профиль.",
"If you forget something": "Если Вы что-то забыли, воспользуйтесь страницей 'Tutorial'.",
"Support us": "Вы можете поддержать нас, поставив звезду",
"our repo": "нашему репозиторию",
"Got it": "Понятненько!",
"Admin": "Админ",
"Admin page": "Кабинет администратора",
"Course name": "Название курса",
"Team size": "Количество участников",
"Enter team size": "Введите количество участников",
"Add new course": "Добавить новый курс",
"Courses list": "Список курсов",
"Show": "Показать",
"Search course": "Найти курс...",
"Active(status)": "Активный",
"Active": "Активные",
"Terminate": "Закрыть",
"Terminate(status)": "Законченный",
"Terminated": "Законченные",
"Edit course": "Редактировать курс",
"Course was created": "Курс создан!",
"If you want to change something in new course": "Если Вы хотите что-то изменить в новом курсе, вы можете его отредактирвоать",
"New course name": "Новое название курса",
"New team size": "Новое количество учасников",
"Enter new course name": "Введите новое название курса",
"Enter new team size": "Введите новое количество участников",
"Please, enter new course information": "Пожалуйста, введите новую информацию о курсе",
"Please, enter unique course name": "Имя курса должно быть уникально",
"Please, enter correct course name": "Должен содержать текущий год",
"Please, enter correct team size": "Пожалуйста, введите корректное количество участников (2-9)",
"Terminate course": "Закрыть курс",
"Activate course": "Активировать курс",
"Close": "Закрыть",
"Save changes": "Сохранить изменения",
"Unset": "Не заданo"
}
================================================
FILE: src/types.ts
================================================
export type User = {
id: string;
firstName: string;
lastName: string;
github: string;
telegram: string | null;
discord: string;
score: number;
country: string;
city: string;
avatar: string;
isAdmin: boolean;
courses: Course[];
email: string | null;
courseIds: string[];
teamIds: string[];
teams: Team[];
};
export interface Course {
id: string;
name: string;
isActive: boolean;
teamSize: number | null;
teamIds?: string[];
userIds?: string[];
teams?: Team[];
users?: User[];
}
export type Team = {
id: string;
number: number;
password: string;
courseId: string;
socialLink: string;
memberIds: string[];
course: Course;
members: User[];
};
export type TeamList = {
count: number;
results: Team[];
};
export type TFilterForm = {
discord: string | null;
github: string | null;
location: string | null;
courseName: string | null;
sortingOrder: string;
teamFilter: string;
};
export type UpdateUserInput = {
id: string;
firstName: string;
lastName: string;
email?: string;
telegram: string;
discord: string;
score?: number;
country: string;
city: string;
courseIds: string[];
};
export type AddUserToTeamInput = {
userId: string;
courseId: string;
teamPassword: string;
};
export type RemoveUserFromTeamInput = {
userId: string;
teamId: string;
page: number;
courseId: string;
};
export type CreateTeamInput = {
socialLink: string;
courseId: string;
ownerId: string;
page: number;
};
export type CreateCourseInput = {
name: string;
isActive: boolean;
teamSize: number;
};
export type UpdateCourseInput = {
id: string;
name: string;
isActive: boolean;
teamSize: number;
};
export type UpdateTeamInput = {
id: string;
socialLink: string;
};
export type StateTeamsList = {
isActiveModalExpel: boolean;
isActiveModalLeave: boolean;
isActiveModalJoin: boolean;
isActiveModalCreateTeam: boolean;
isActiveModalCreated: boolean;
isActiveModalUpdateSocialLink: boolean;
isActiveModalRemoveCourse: boolean;
isActiveModalSortStudents: boolean;
isActiveModalLeavePage: boolean;
isActiveModalCreatedCourse: boolean;
isActiveModalEditCourse: boolean;
teamMemberExpelId: string;
teamPassword: string;
socialLink: string;
};
export type UserFilterInput = {
discord: string | null;
github: string | null;
location: string | null;
courseName: string | null;
sortingOrder: string;
teamFilter: boolean;
};
export type RemoveUserFromCourseInput = {
userId: string;
teamId?: string | null;
courseId: string;
page: number;
};
export type StateStudentsTable = {
userData: User;
filterData: TFilterForm;
};
export type StateLoginPage = {
loginToken: string | null;
currCourse: Course;
currLanguage: string;
isCommonError: boolean;
isBurgerMenuOpen: boolean;
isEditProfileDataChange: boolean;
pathToThePage: string;
isTourOpen: boolean;
};
export type State = {
studentsTableReducer: StateStudentsTable;
teamsListReducer: StateTeamsList;
loginPageReducer: StateLoginPage;
};
export type TNavLink = {
name: string;
isAlwaysVisible: boolean;
};
================================================
FILE: src/typography/common.css
================================================
*,
*::before,
*::after {
box-sizing: border-box;
}
:root {
--BG_COLOR: #f2f8fd;
--OVERLAY_COLOR: rgba(54, 61, 72, 0.3);
--SCROLL_THUMB_COLOR: #1e33570d;
--WHITE_COLOR: #ffffff;
}
body {
margin: 0;
font: normal 16px/24px "Poppins", sans-serif;
background-color: var(--BG_COLOR);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
input {
background-color: var(--BG_COLOR);
}
================================================
FILE: src/typography/fonts.css
================================================
@font-face {
font-style: normal;
font-weight: 400;
font-family: "Poppins";
src: url("../assets/fonts/Poppins-Regular-400.otf") format("otf"),
url("../assets/fonts/Poppins-Regular-400.woff2") format("woff2"),
url("../assets/fonts/Poppins-Regular-400.woff") format("woff"),
url("../assets/fonts/Poppins-Regular-400.ttf") format("truetype");
}
@font-face {
font-style: normal;
font-weight: 500;
font-family: "Poppins";
src: url("../assets/fonts/Poppins-Medium-500.otf") format("otf"),
url("../assets/fonts/Poppins-Medium-500.woff") format("woff"),
url("../assets/fonts/Poppins-Medium-500.woff2") format("woff2")
url("../assets/fonts/Poppins-Medium-500.ttf") format("truetype");
}
@font-face {
font-style: normal;
font-weight: 600;
font-family: "Poppins";
src: url("../assets/fonts/Poppins-SemiBold-600.otf") format("otf"),
url("../assets/fonts/Poppins-SemiBold-600.woff") format("woff"),
url("../assets/fonts/Poppins-SemiBold-600.woff2") format("woff2"),
url("../assets/fonts/Poppins-SemiBold-600.ttf") format("truetype");
}
@font-face {
font-style: normal;
font-weight: 700;
font-family: "Poppins";
src: url("../assets/fonts/Poppins-Bold-700.otf") format("otf"),
url("../assets/fonts/Poppins-Bold-700.woff") format("woff"),
url("../assets/fonts/Poppins-Bold-700.woff2") format("woff2"),
url("../assets/fonts/Poppins-Bold-700.ttf") format("truetype");
}
================================================
FILE: src/typography/index.ts
================================================
import styled, { css } from 'styled-components';
import { ReactComponent as RSLogoIcon } from 'assets/svg/rslogo.svg';
import {
WHITE_COLOR,
BG_COLOR,
MAIN1_COLOR,
DARK_TEXT_COLOR,
LIGHT_TEXT_COLOR,
TABLE_POPUP_BORDER_COLOR,
} from 'appConstants/colors';
interface StyledTextProps {
color?: string;
fontSize?: string;
lineHeight?: string;
marginBottom?: string;
}
type ButtonProps = {
bgc?: string;
color?: string;
mr?: string;
};
type TRSLogoProps = {
login: string | null;
};
type ModalInputProps = {
mt?: string;
blink?: boolean;
autoComplete?: string;
};
export const RSLogo = styled(RSLogoIcon)`
width: 84px;
height: 30px;
margin-top: 5px;
margin-bottom: 22%;
path {
fill: ${({ login }) => (login ? WHITE_COLOR : DARK_TEXT_COLOR)};
}
@media (max-width: 992px) {
width: 80px;
height: 28px;
}
@media (max-width: 768px) {
width: 76px;
height: 24px;
}
@media (max-width: 550px) {
width: 72px;
height: 22px;
}
@media (max-width: 440px) {
width: 70px;
height: 20px;
}
`;
const StyledText = css`
color: ${(props) => props.color || DARK_TEXT_COLOR};
font-size: ${(props) => props.fontSize || '16px'};
line-height: ${(props) => props.lineHeight || '24px'};
`;
export const TextRegular = css`
${StyledText};
font-weight: 400;
`;
export const TextMedium = css`
${StyledText};
font-weight: 500;
`;
export const TextSemiBold = css`
${StyledText};
font-weight: 600;
`;
export const TextBold = css`
${StyledText};
font-weight: 700;
`;
export const GeneralAdaptiveFont = css`
@media (max-width: 1200px) {
font-size: 0.95rem;
line-height: 23px;
}
@media (max-width: 992px) {
font-size: 0.9rem;
line-height: 22px;
}
@media (max-width: 768px) {
font-size: 0.825rem;
line-height: 21px;
}
@media (max-width: 550px) {
font-size: 0.8rem;
line-height: 20px;
}
@media (max-width: 440px) {
font-size: 0.68rem;
line-height: 18px;
}
`;
export const HeaderAdaptiveFont = css`
font-size: 1rem;
line-height: 24px;
@media (max-width: 768px) {
font-size: 0.925rem;
line-height: 22px;
}
@media (max-width: 440px) {
font-size: 0.85rem;
line-height: 20px;
}
`;
export const H1AdaptiveFont = css`
font-size: 30px;
line-height: 45px;
@media (max-width: 1200px) {
font-size: 28px;
line-height: 40px;
}
@media (max-width: 992px) {
font-size: 26px;
line-height: 38px;
}
@media (max-width: 768px) {
font-size: 24px;
line-height: 36px;
}
@media (max-width: 550px) {
font-size: 22px;
line-height: 32px;
}
@media (max-width: 440px) {
font-size: 20px;
line-height: 28px;
}
`;
export const H2AdaptiveFont = css`
@media (max-width: 1200px) {
font-size: 26px;
line-height: 35px;
}
@media (max-width: 992px) {
font-size: 24px;
line-height: 30px;
}
@media (max-width: 768px) {
font-size: 22px;
line-height: 25px;
}
@media (max-width: 550px) {
font-size: 20px;
line-height: 25px;
}
@media (max-width: 440px) {
font-size: 17px;
line-height: 20px;
}
`;
export const GeneralButtonPadding = css`
padding: 13px 50px;
@media (max-width: 1200px) {
padding: 11px 45px;
}
@media (max-width: 992px) {
padding: 14px 40px;
}
@media (max-width: 768px) {
padding: 12px 30px;
}
@media (max-width: 550px) {
padding: 10px 25px;
}
@media (max-width: 440px) {
padding: 10px 20px;
}
`;
export const SVGArrowAdaptive = css`
@media (max-width: 992px) {
width: 13px;
height: 13px;
}
@media (max-width: 768px) {
width: 12px;
height: 12px;
}
@media (max-width: 550px) {
width: 11px;
height: 11px;
}
@media (max-width: 440px) {
width: 10px;
height: 10px;
}
`;
export const SVGParamsAdaptive = css`
width: 16px;
height: 16px;
@media (max-width: 768px) {
width: 15px;
height: 15px;
}
@media (max-width: 550px) {
width: 14px;
height: 14px;
}
@media (max-width: 440px) {
width: 13px;
height: 13px;
}
`;
export const AdditionalWrapper = styled.div`
width: 100%;
height: 60px;
@media screen and (max-width: 768px) {
height: 20px;
}
`;
export const ScrollBar = css`
scrollbar-color: transparent ${TABLE_POPUP_BORDER_COLOR};
scrollbar-width: 8px;
&::-webkit-scrollbar {
width: 8px;
background-color: transparent;
}
&::-webkit-scrollbar-thumb {
background-color: ${TABLE_POPUP_BORDER_COLOR};
}
`;
export const MainComponentHeight = css`
height: calc(100vh - 191px);
@media (max-width: 992px) {
height: calc(100vh - 181px);
}
@media (max-width: 880px) {
height: calc(100vh - 161px);
}
@media (max-width: 768px) {
height: calc(100vh - 146px);
}
@media (max-width: 550px) {
height: calc(100vh - 141px);
}
@media (max-width: 440px) {
height: calc(100vh - 131px);
}
`;
export const ContentPageWrapper = styled.div`
display: flex;
width: 100%;
justify-content: center;
overflow-y: scroll;
${ScrollBar};
${MainComponentHeight};
`;
export const PageTitle = styled.h1`
${TextBold};
font-size: ${(props) => props.fontSize || '30px'};
line-height: ${(props) => props.lineHeight || '45px'};
margin-top: 0;
${H1AdaptiveFont};
`;
export const PageSubTitle = styled.h2`
${TextSemiBold};
color: ${(props) => props.color || WHITE_COLOR};
font-size: ${(props) => props.fontSize || '24px'};
line-height: ${(props) => props.lineHeight || '36px'};
@media screen and (max-width: 768px) {
font-size: 16px;
}
`;
export const Button = styled.button`
${TextSemiBold};
margin-right: ${({ mr }) => mr || 0};
border-radius: 20px;
border: none;
outline: none;
cursor: pointer;
background-color: ${({ bgc }) => bgc || MAIN1_COLOR};
color: ${({ color }) => color || WHITE_COLOR};
${GeneralAdaptiveFont};
${GeneralButtonPadding}
`;
export const TeamButton = styled(Button)`
padding: 11px 23px;
@media (max-width: 650px) {
padding: 8px 16px;
}
`;
export const CourseButton = styled(TeamButton)`
background-color: ${WHITE_COLOR};
color: ${DARK_TEXT_COLOR};
border-radius: 10px;
`;
export const InvertedButton = styled(Button)`
background-color: ${BG_COLOR};
color: ${MAIN1_COLOR};
`;
export const Label = styled.label`
${TextRegular};
${GeneralAdaptiveFont};
max-width: 300px;
margin-bottom: ${(props) => props.marginBottom || '10px'};
color: ${(props) => props.color || LIGHT_TEXT_COLOR};
`;
export const Input = styled.input`
${TextMedium};
width: 300px;
padding: 8px 15px;
border-radius: 10px;
border: none;
background-color: ${BG_COLOR};
color: ${(props) => props.color || DARK_TEXT_COLOR};
outline: none;
${GeneralAdaptiveFont};
@media (max-width: 440px) {
width: 100%;
}
&::placeholder {
color: ${LIGHT_TEXT_COLOR};
}
`;
export const ModalInput = styled(Input)`
@keyframes blinkInput {
from {
background-color: rgb(101, 80, 246, 0.5);
}
to {
background-color: ${BG_COLOR};
}
}
animation: ${({ blink }) => (blink ? 'blinkInput 1s' : 'none')};
margin-top: ${({ mt }) => mt || '20px'};
`;
export const Select = styled.div`
display: grid;
position: relative;
grid-template-areas: 'select';
align-items: center;
width: 100%;
background-color: ${BG_COLOR};
border-radius: 10px;
cursor: pointer;
&:after {
position: absolute;
content: '*';
grid-area: select;
width: 12px;
height: 7px;
background-color: ${LIGHT_TEXT_COLOR};
clip-path: polygon(100% 0%, 50% 70%, 0% 0%, 0% 40%, 50% 100%, 100% 40%);
justify-self: end;
right: 15px;
}
`;
export const SelectInner = styled.select`
${TextMedium};
margin: 0;
width: 100%;
grid-area: select;
padding: 8px 15px;
border: none;
border-radius: 10px;
background-color: transparent;
color: ${(props) => props.color || LIGHT_TEXT_COLOR};
outline: none;
${GeneralAdaptiveFont}
appearance: none;
z-index: 1;
cursor: pointer;
option {
color: ${DARK_TEXT_COLOR};
}
`;
export const ButtonsBlock = styled.div`
display: flex;
justify-content: space-around;
margin-top: 30px;
margin-bottom: 10px;
`;
================================================
FILE: src/typography/normalize.css
================================================
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
/* Document
========================================================================== */
/**
* 1. Correct the line height in all browsers.
* 2. Prevent adjustments of font size after orientation changes in iOS.
*/
html {
line-height: 1.15; /* 1 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/* Sections
========================================================================== */
/**
* Remove the margin in all browsers.
*/
body {
margin: 0;
}
/**
* Render the `main` element consistently in IE.
*/
main {
display: block;
}
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/
h1 {
margin: 0.67em 0;
font-size: 2em;
}
/* Grouping content
========================================================================== */
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
hr {
box-sizing: content-box; /* 1 */
height: 0; /* 1 */
overflow: visible; /* 2 */
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
pre {
font-size: 1em; /* 2 */
font-family: monospace, monospace; /* 1 */
}
/* Text-level semantics
========================================================================== */
/**
* Remove the gray background on active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* 1. Remove the bottom border in Chrome 57-
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
text-decoration: underline; /* 2 */
text-decoration: underline dotted; /* 2 */
border-bottom: none; /* 1 */
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp {
font-size: 1em; /* 2 */
font-family: monospace, monospace; /* 1 */
}
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
position: relative;
font-size: 75%;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/* Embedded content
========================================================================== */
/**
* Remove the border on images inside links in IE 10.
*/
img {
border-style: none;
}
/* Forms
========================================================================== */
/**
* 1. Change the font styles in all browsers.
* 2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
margin: 0; /* 2 */
font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */
font-family: inherit; /* 1 */
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input {
/* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select {
/* 1 */
text-transform: none;
}
/**
* Correct the inability to style clickable types in iOS and Safari.
*/
button,
[type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
padding: 0;
border-style: none;
}
/**
* Restore the focus styles unset by the previous rule.
*/
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
/**
* Correct the padding in Firefox.
*/
fieldset {
padding: 0.35em 0.75em 0.625em;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/
legend {
display: table; /* 1 */
box-sizing: border-box; /* 1 */
max-width: 100%; /* 1 */
padding: 0; /* 3 */
color: inherit; /* 2 */
white-space: normal; /* 1 */
}
/**
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
vertical-align: baseline;
}
/**
* Remove the default vertical scrollbar in IE 10+.
*/
textarea {
overflow: auto;
}
/**
* 1. Add the correct box sizing in IE 10.
* 2. Remove the padding in IE 10.
*/
[type="checkbox"],
[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type="search"] {
outline-offset: -2px; /* 2 */
-webkit-appearance: textfield; /* 1 */
}
/**
* Remove the inner padding in Chrome and Safari on macOS.
*/
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
font: inherit; /* 2 */
-webkit-appearance: button; /* 1 */
}
/* Interactive
========================================================================== */
/*
* Add the correct display in Edge, IE 10+, and Firefox.
*/
details {
display: block;
}
/*
* Add the correct display in all browsers.
*/
summary {
display: list-item;
}
/* Misc
========================================================================== */
/**
* Add the correct display in IE 10+.
*/
template {
display: none;
}
/**
* Add the correct display in IE 10.
*/
[hidden] {
display: none;
}
================================================
FILE: src/utils/isFieldValid.ts
================================================
export const isFieldValid = (
value: string,
validateRules: any,
needValidate: boolean,
setInputValid: (valid: boolean) => void,
setErrorMessage: (message: string) => void,
isValueUniq?: (courseName: string) => boolean
) => {
if (!needValidate) return true;
const trimmedValueLength = value.trim().length;
let valid = true;
if (validateRules.maxLength) {
valid = trimmedValueLength < validateRules.maxLength.value + 1 && valid;
if (!(trimmedValueLength < validateRules.maxLength.value + 1)) {
setErrorMessage(validateRules.maxLength.message);
}
}
if (validateRules.minLength) {
valid = trimmedValueLength >= validateRules.minLength.value && valid;
if (!(trimmedValueLength >= validateRules.minLength.value)) {
setErrorMessage(validateRules.minLength.message);
}
}
if (validateRules.pattern) {
const regExp = new RegExp(validateRules.pattern.value);
valid = regExp.test(value) && valid;
if (!regExp.test(value)) {
setErrorMessage(validateRules.pattern.message);
}
}
if (validateRules.uniq && isValueUniq) {
const isFieldValueUniq = isValueUniq(value.trim());
valid = isFieldValueUniq && valid;
if (!isFieldValueUniq) {
setErrorMessage(validateRules.uniq.message);
}
}
setInputValid(valid);
};
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"baseUrl": "./src"
},
"include": [
"src"
]
}