Merge pull request 'admin-panel' (#33) from admin-panel into master

Reviewed-on: http://git.plannaplan.pl/y0rune/frontend/pulls/33
Reviewed-by: Maciej <glowackimaciej97@gmail.com>
This commit is contained in:
Marcin Woźniak 2020-12-04 16:41:16 +01:00
commit 1aeb24c345
51 changed files with 15477 additions and 2817 deletions

1
.env
View File

@ -1 +0,0 @@
REACT_APP_API_URL=http://localhost:1285

View File

@ -1,2 +0,0 @@
node_modules
build

View File

@ -1,28 +0,0 @@
// module.exports = {
// parser: '@typescript-eslint/parser',
// extends: [
// 'plugin:react/recommended',
// 'plugin:@typescript-eslint/recommended',
// 'prettier/@typescript-eslint',
// 'plugin:prettier/recommended',
// 'plugin:react-hooks/recommended',
// ],
// parserOptions: {
// ecmaVersion: 2020,
// sourceType: 'module',
// ecmaFeatures: {
// jsx: true,
// },
// },
// rules: {
// '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_|^req|^next' }],
// '@typescript-eslint/no-explicit-any': 0,
// '@typescript-eslint/explicit-function-return-type': 0,
// 'react/prop-types': 0,
// },
// settings: {
// react: {
// version: 'detect',
// },
// },
// };

2
.gitignore vendored
View File

@ -1 +1,3 @@
node_modules
.env
build

15
.vscode/launch.json vendored
View File

@ -1,15 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:3000",
"webRoot": "${workspaceFolder}"
}
]
}

View File

@ -1,24 +0,0 @@
{
"files": {
"main.js": "/static/js/main.993a2e72.chunk.js",
"main.js.map": "/static/js/main.993a2e72.chunk.js.map",
"runtime-main.js": "/static/js/runtime-main.38573804.js",
"runtime-main.js.map": "/static/js/runtime-main.38573804.js.map",
"static/js/2.f941ed96.chunk.js": "/static/js/2.f941ed96.chunk.js",
"static/js/2.f941ed96.chunk.js.map": "/static/js/2.f941ed96.chunk.js.map",
"index.html": "/index.html",
"precache-manifest.36f396009b702c50e7d07732240c69e5.js": "/precache-manifest.36f396009b702c50e7d07732240c69e5.js",
"service-worker.js": "/service-worker.js",
"static/js/2.f941ed96.chunk.js.LICENSE.txt": "/static/js/2.f941ed96.chunk.js.LICENSE.txt",
"static/media/PL.png": "/static/media/PL.6e9ee893.png",
"static/media/UK.png": "/static/media/UK.b4dad475.png",
"static/media/close.svg": "/static/media/close.464128e7.svg",
"static/media/search.svg": "/static/media/search.fa0d12ae.svg",
"static/media/user.png": "/static/media/user.4ba6e2a4.png"
},
"entrypoints": [
"static/js/runtime-main.38573804.js",
"static/js/2.f941ed96.chunk.js",
"static/js/main.993a2e72.chunk.js"
]
}

View File

@ -1 +0,0 @@
<!doctype html><html lang="pl"><head><meta charset="utf-8"/><link rel="icon" href="/logo.svg"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site created using create-react-app"/><link rel="apple-touch-icon" href="/logo.svg"/><link rel="manifest" href="/manifest.json"/><title>PlanNaPlan</title></head><body><noscript>Potrzebujesz włączyć JavaScript, żeby otworzyć tę aplikację<br>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script>!function(e){function r(r){for(var n,l,a=r[0],p=r[1],f=r[2],c=0,s=[];c<a.length;c++)l=a[c],Object.prototype.hasOwnProperty.call(o,l)&&o[l]&&s.push(o[l][0]),o[l]=0;for(n in p)Object.prototype.hasOwnProperty.call(p,n)&&(e[n]=p[n]);for(i&&i(r);s.length;)s.shift()();return u.push.apply(u,f||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,a=1;a<t.length;a++){var p=t[a];0!==o[p]&&(n=!1)}n&&(u.splice(r--,1),e=l(l.s=t[0]))}return e}var n={},o={1:0},u=[];function l(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,l),t.l=!0,t.exports}l.m=e,l.c=n,l.d=function(e,r,t){l.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},l.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},l.t=function(e,r){if(1&r&&(e=l(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(l.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)l.d(t,n,function(r){return e[r]}.bind(null,n));return t},l.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return l.d(r,"a",r),r},l.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},l.p="/";var a=this.webpackJsonpplannaplan=this.webpackJsonpplannaplan||[],p=a.push.bind(a);a.push=r,a=a.slice();for(var f=0;f<a.length;f++)r(a[f]);var i=p;t()}([])</script><script src="/static/js/2.f941ed96.chunk.js"></script><script src="/static/js/main.993a2e72.chunk.js"></script></body></html>

View File

@ -1,13 +0,0 @@
{
"short_name": "PlanNaPlan",
"name": "PlanNaPlan",
"icons": [
{
"src": "logo.svg"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@ -1,42 +0,0 @@
self.__precacheManifest = (self.__precacheManifest || []).concat([
{
"revision": "53d5f0388bed11f07f899035cb978454",
"url": "/index.html"
},
{
"revision": "4d480560b5cfff1aa5a8",
"url": "/static/js/2.f941ed96.chunk.js"
},
{
"revision": "68ba00d5eb083746d913686923a3d904",
"url": "/static/js/2.f941ed96.chunk.js.LICENSE.txt"
},
{
"revision": "209120e09e641f02e991",
"url": "/static/js/main.993a2e72.chunk.js"
},
{
"revision": "c20b5bb64d57e939de77",
"url": "/static/js/runtime-main.38573804.js"
},
{
"revision": "6e9ee893741bee46177f72398176ad9e",
"url": "/static/media/PL.6e9ee893.png"
},
{
"revision": "b4dad47599118a028176ceb8bbbc7a02",
"url": "/static/media/UK.b4dad475.png"
},
{
"revision": "464128e7e5f72a712b7c752996e86b58",
"url": "/static/media/close.464128e7.svg"
},
{
"revision": "fa0d12ae1b4380d1515aed6c5e5cbc07",
"url": "/static/media/search.fa0d12ae.svg"
},
{
"revision": "4ba6e2a4136a8a9ce072ee9c1e403f3a",
"url": "/static/media/user.4ba6e2a4.png"
}
]);

View File

@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@ -1,39 +0,0 @@
/**
* Welcome to your Workbox-powered service worker!
*
* You'll need to register this file in your web app and you should
* disable HTTP caching for this file too.
* See https://goo.gl/nhQhGp
*
* The rest of the code is auto-generated. Please don't update this file
* directly; instead, make changes to your Workbox build configuration
* and re-run your build process.
* See https://goo.gl/2aRDsh
*/
importScripts("https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js");
importScripts(
"/precache-manifest.36f396009b702c50e7d07732240c69e5.js"
);
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
workbox.core.clientsClaim();
/**
* The workboxSW.precacheAndRoute() method efficiently caches and responds to
* requests for URLs in the manifest.
* See https://goo.gl/S9QRab
*/
self.__precacheManifest = [].concat(self.__precacheManifest || []);
workbox.precaching.precacheAndRoute(self.__precacheManifest, {});
workbox.routing.registerNavigationRoute(workbox.precaching.getCacheKeyForURL("/index.html"), {
blacklist: [/^\/_/,/\/[^/?]+\.[^/]+$/],
});

File diff suppressed because one or more lines are too long

View File

@ -1,58 +0,0 @@
/*
object-assign
(c) Sindre Sorhus
@license MIT
*/
/**
* @license
* Lodash <https://lodash.com/>
* Copyright OpenJS Foundation and other contributors <https://openjsf.org/>
* Released under MIT license <https://lodash.com/license>
* Based on Underscore.js 1.8.3 <http://underscorejs.org/LICENSE>
* Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
*/
/**
* A better abstraction over CSS.
*
* @copyright Oleg Isonen (Slobodskoi) / Isonen 2014-present
* @website https://github.com/cssinjs/jss
* @license MIT
*/
/** @license React v0.19.1
* scheduler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v16.13.1
* react-dom.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v16.13.1
* react-is.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v16.13.1
* react.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,2 +0,0 @@
!function(e){function r(r){for(var n,l,a=r[0],p=r[1],f=r[2],c=0,s=[];c<a.length;c++)l=a[c],Object.prototype.hasOwnProperty.call(o,l)&&o[l]&&s.push(o[l][0]),o[l]=0;for(n in p)Object.prototype.hasOwnProperty.call(p,n)&&(e[n]=p[n]);for(i&&i(r);s.length;)s.shift()();return u.push.apply(u,f||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,a=1;a<t.length;a++){var p=t[a];0!==o[p]&&(n=!1)}n&&(u.splice(r--,1),e=l(l.s=t[0]))}return e}var n={},o={1:0},u=[];function l(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,l),t.l=!0,t.exports}l.m=e,l.c=n,l.d=function(e,r,t){l.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},l.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},l.t=function(e,r){if(1&r&&(e=l(e)),8&r)return e;if(4&r&&"object"===typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(l.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)l.d(t,n,function(r){return e[r]}.bind(null,n));return t},l.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return l.d(r,"a",r),r},l.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},l.p="/";var a=this.webpackJsonpplannaplan=this.webpackJsonpplannaplan||[],p=a.push.bind(a);a.push=r,a=a.slice();for(var f=0;f<a.length;f++)r(a[f]);var i=p;t()}([]);
//# sourceMappingURL=runtime-main.38573804.js.map

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@ -1 +0,0 @@
<?xml version="1.0" ?><svg height="48" viewBox="0 0 48 48" width="48" xmlns="http://www.w3.org/2000/svg"><path d="M38 12.83l-2.83-2.83-11.17 11.17-11.17-11.17-2.83 2.83 11.17 11.17-11.17 11.17 2.83 2.83 11.17-11.17 11.17 11.17 2.83-2.83-11.17-11.17z"/><path d="M0 0h48v48h-48z" fill="none"/></svg>

Before

Width:  |  Height:  |  Size: 297 B

View File

@ -1 +0,0 @@
<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'><svg height="512px" id="Layer_1" style="enable-background:new 0 0 512 512;" version="1.1" viewBox="0 0 512 512" width="512px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M344.5,298c15-23.6,23.8-51.6,23.8-81.7c0-84.1-68.1-152.3-152.1-152.3C132.1,64,64,132.2,64,216.3 c0,84.1,68.1,152.3,152.1,152.3c30.5,0,58.9-9,82.7-24.4l6.9-4.8L414.3,448l33.7-34.3L339.5,305.1L344.5,298z M301.4,131.2 c22.7,22.7,35.2,52.9,35.2,85c0,32.1-12.5,62.3-35.2,85c-22.7,22.7-52.9,35.2-85,35.2c-32.1,0-62.3-12.5-85-35.2 c-22.7-22.7-35.2-52.9-35.2-85c0-32.1,12.5-62.3,35.2-85c22.7-22.7,52.9-35.2,85-35.2C248.5,96,278.7,108.5,301.4,131.2z"/></svg>

Before

Width:  |  Height:  |  Size: 808 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

4777
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,35 +4,41 @@
"private": true,
"dependencies": {
"@material-ui/core": "^4.10.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "^4.0.0-alpha.56",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.5.0",
"@testing-library/user-event": "^7.2.1",
"axios": "^0.19.2",
"notistack": "^1.0.1",
"react": "^16.13.1",
"react": "16.8.0",
"react-click-away-listener": "^1.4.3",
"react-dom": "^16.13.1",
"react-scripts": "3.4.1",
"styled-components": "^5.1.1"
},
"devDependencies": {
"@types/jest": "^24.9.1",
"@types/lodash": "^4.14.162",
"@types/node": "^12.12.54",
"@types/react": "^16.9.46",
"@types/react-dom": "^16.9.8",
"@types/styled-components": "^5.1.2",
"prettier": "^2.0.5",
"@typescript-eslint/eslint-plugin": "^4.8.1",
"@typescript-eslint/parser": "^4.8.1",
"eslint-config-prettier": "^6.15.0",
"eslint-config-react-app": "^6.0.0",
"eslint-loader": "^4.0.2",
"eslint-plugin-prettier": "^3.1.4",
"eslint-plugin-react": "^7.21.5",
"prettier": "^2.2.0",
"typescript": "^3.9.7"
},
"optionalDependencies": {},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"lint": "eslint src/*.{js,ts,tsx} --quiet --fix"
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [

View File

@ -1,24 +1,42 @@
<!DOCTYPE html>
<html lang="pl">
<head>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/logo.svg" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Web site created using create-react-app" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo.svg" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link href="https://fonts.googleapis.com/css2?family=Roboto+Condensed:wght@400;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap" rel="stylesheet">
<title>PlanNaPlan</title>
</head>
<body>
<noscript>Potrzebujesz włączyć JavaScript, żeby otworzyć tę aplikację</br>You need to enable JavaScript to run this
app.</noscript>
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View File

@ -1,25 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid" width="200" height="200" viewBox="0 0 200 200">
<defs>
<style>
.cls-1 {
fill: #6d6e71;
}
.cls-1, .cls-2, .cls-3 {
fill-rule: evenodd;
}
.cls-2 {
fill: #f9ca24;
}
.cls-3 {
fill: #1761a0;
}
</style>
</defs>
<path d="M22.000,156.000 L23.000,154.000 L24.000,153.000 L25.000,152.000 L26.000,151.000 L28.000,150.000 L33.000,150.000 L35.000,151.000 L36.000,152.000 L37.000,153.000 L38.000,154.000 L39.000,156.000 L39.000,161.000 L38.000,163.000 L37.000,164.000 L36.000,165.000 L35.000,166.000 L33.000,167.000 L28.000,167.000 L26.000,166.000 L25.000,165.000 L24.000,164.000 L23.000,163.000 L22.000,161.000 L22.000,156.000 ZM26.000,156.000 L27.000,155.000 L28.000,154.000 L33.000,154.000 L34.000,155.000 L35.000,156.000 L35.000,161.000 L34.000,162.000 L33.000,163.000 L28.000,163.000 L27.000,162.000 L26.000,161.000 L26.000,156.000 Z" class="cls-1"/>
<path d="M10.000,75.000 L100.000,131.000 L190.000,75.000 L100.000,20.000 L10.000,75.000 Z" class="cls-2"/>
<path d="M84.000,52.000 L86.000,54.000 L32.000,89.000 L32.000,153.000 L29.000,153.000 L29.000,88.000 L84.000,52.000 Z" class="cls-1"/>
<path d="M45.000,102.000 L45.000,143.000 L100.000,180.000 L155.000,143.000 L155.000,102.000 L100.000,136.000 L45.000,102.000 Z" class="cls-3"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1,13 +0,0 @@
{
"short_name": "PlanNaPlan",
"name": "PlanNaPlan",
"icons": [
{
"src": "logo.svg"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

1
src/assets/history.svg Normal file
View File

@ -0,0 +1 @@
<svg id="Capa_1" enable-background="new 0 0 551.13 551.13" height="512" viewBox="0 0 551.13 551.13" width="512" xmlns="http://www.w3.org/2000/svg"><path d="m275.531 172.228-.05 120.493c0 4.575 1.816 8.948 5.046 12.177l86.198 86.181 24.354-24.354-81.153-81.136.05-113.361z"/><path d="m310.011 34.445c-121.23 0-221.563 90.033-238.367 206.674h-71.644l86.114 86.114 86.114-86.114h-65.78c16.477-97.589 101.355-172.228 203.563-172.228 113.966 0 206.674 92.707 206.674 206.674s-92.707 206.674-206.674 206.674c-64.064 0-123.469-28.996-162.978-79.555l-27.146 21.192c46.084 58.968 115.379 92.808 190.124 92.808 132.955 0 241.119-108.181 241.119-241.119s-108.164-241.119-241.119-241.12z"/></svg>

After

Width:  |  Height:  |  Size: 684 B

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

1
src/assets/plan.svg Normal file
View File

@ -0,0 +1 @@
<svg enable-background="new 0 0 24 24" height="512" viewBox="0 0 24 24" width="512" xmlns="http://www.w3.org/2000/svg"><path d="m6 0h-5c-.552 0-1 .448-1 1v5c0 .552.448 1 1 1h5c.552 0 1-.448 1-1v-5c0-.552-.448-1-1-1z"/><path d="m15.5 6v-5c0-.552-.448-1-1-1h-5c-.552 0-1 .448-1 1v5c0 .552.448 1 1 1h5c.552 0 1-.448 1-1z"/><path d="m15.5 9.5c0-.552-.448-1-1-1h-6-1.5-6c-.552 0-1 .448-1 1v5c0 .552.448 1 1 1h13.5c.552 0 1-.448 1-1z"/><path d="m23 0h-5c-.552 0-1 .448-1 1v5c0 .552.448 1 1 1h5c.552 0 1-.448 1-1v-5c0-.552-.448-1-1-1z"/><path d="m0 18v5c0 .552.448 1 1 1h13.5c.552 0 1-.448 1-1v-5c0-.552-.448-1-1-1h-13.5c-.552 0-1 .448-1 1z"/><path d="m18 15.5h5c.552 0 1-.448 1-1v-5c0-.552-.448-1-1-1h-5c-.552 0-1 .448-1 1v5c0 .552.448 1 1 1z"/><path d="m18 24h5c.552 0 1-.448 1-1v-5c0-.552-.448-1-1-1h-5c-.552 0-1 .448-1 1v5c0 .552.448 1 1 1z"/></svg>

After

Width:  |  Height:  |  Size: 846 B

53
src/assets/statistics.svg Normal file
View File

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 478 478" style="enable-background:new 0 0 478 478;" xml:space="preserve">
<g>
<g>
<path d="M119.5,187.75H17.1c-9.4,0-17,7.6-17.1,17.1v256c0,9.5,7.7,17.1,17.1,17.1h102.4c9.5,0,17.1-7.7,17.1-17.1v-256
C136.6,195.35,128.9,187.75,119.5,187.75z"/>
</g>
</g>
<g>
<g>
<path d="M290.2,0.05H187.8c-9.4,0-17.1,7.6-17.1,17v443.8c0,9.5,7.7,17.1,17.1,17.1h102.4c9.5,0,17.1-7.7,17.1-17.1V17.15
C307.3,7.65,299.6,0.05,290.2,0.05z"/>
</g>
</g>
<g>
<g>
<path d="M460.9,136.55H358.5c-9.5,0-17.1,7.6-17.1,17.1v307.2c0,9.5,7.7,17.1,17.1,17.1h102.4c9.5,0,17.1-7.7,17.1-17.1v-307.2
C478,144.15,470.3,136.55,460.9,136.55z"/>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

133
src/components/Admin.tsx Normal file
View File

@ -0,0 +1,133 @@
import React, { useState, MouseEvent } from 'react';
import styled from 'styled-components/macro';
import Plan from '../assets/plan.svg';
import History from '../assets/history.svg';
import Statistics from '../assets/statistics.svg';
import { Scheduler } from './Scheduler';
import { Rightbar } from './Rightbar';
const LeftSide = styled.div`
height: 100%;
display: flex;
flex: 1;
flex-direction: column;
background-color: white;
`;
const Wrap = styled.div`
display: flex;
height: calc(100vh - 120px);
background-color: #eceef4;
width: 100%;
`;
const Wrapper = styled.div`
flex: 12;
display: flex;
height: calc(100vh - 120px);
background-color: #eceef4;
`;
interface LeftPanelElement {
isCurrentTab: boolean;
}
const LeftPanelElement = styled.div<LeftPanelElement>`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
flex: 1;
//box-shadow: 0px 2px 2px 0px rgba(0, 0, 0, 0.75);
padding: 20px;
cursor: pointer;
box-shadow: ${({ isCurrentTab }) => (isCurrentTab === true ? `inset 0px 0px 26px 0px rgba(0,0,0,0.55)` : '')};
border-bottom: 1px solid #979797;
`;
const HistoryDiv = styled.div`
flex: 1;
display: flex;
margin-left: 20px;
border-radius: 5px;
height: calc(100vh - 120px);
background-color: red;
`;
const StatsDiv = styled.div`
flex: 1;
display: flex;
margin-left: 20px;
border-radius: 5px;
height: calc(100vh - 120px);
background-color: blue;
`;
const LogoWrapper = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
flex: 2;
margin-left: 10px;
`;
const Text = styled.div`
font-family: 'Roboto', sans-serif;
font-size: 5rem;
user-select: none;
`;
const Logo = styled.img`
width: 500px;
height: 500px;
`;
const Icon = styled.img`
width: 40px;
margin: 5px;
`;
export const Admin = () => {
const [currentTab, setCurrentTab] = useState<null | number>(null);
const handleClick = (e: MouseEvent<HTMLDivElement>) => {
setCurrentTab(Number(e.currentTarget.id));
};
return (
<Wrap>
<LeftSide>
<LeftPanelElement id={'1'} isCurrentTab={currentTab === 1} onClick={handleClick}>
<Icon alt="profile" src={Plan} />
Pokaż plan
</LeftPanelElement>
<LeftPanelElement id={'2'} isCurrentTab={currentTab === 2} onClick={handleClick}>
<Icon alt="history" src={History} />
Historia Zmian
</LeftPanelElement>
<LeftPanelElement id={'3'} isCurrentTab={currentTab === 3} onClick={handleClick}>
<Icon alt="statistics" src={Statistics} />
Statystyki
</LeftPanelElement>
</LeftSide>
<Wrapper>
{currentTab === 1 ? (
<>
<Scheduler />
<Rightbar />
</>
) : currentTab === 2 ? (
<HistoryDiv />
) : currentTab === 3 ? (
<StatsDiv />
) : (
<LogoWrapper>
<Logo alt="logo" src="https://plannaplan.pl/img/logo.svg" />
<Text> plan na plan </Text>
</LogoWrapper>
)}
</Wrapper>
</Wrap>
);
};

View File

@ -1,6 +1,7 @@
import React, { useState, useContext } from 'react';
import React, { useState } from 'react';
import Topbar from './Topbar';
import { Transfer } from './Transfer';
import { Admin } from './Admin';
import { Scheduler } from './Scheduler';
import { Rightbar } from './Rightbar';
import styled from 'styled-components';
@ -8,8 +9,10 @@ import styled from 'styled-components';
const Wrapper = styled.div`
display: flex;
height: calc(100vh - 80px);
background-color: #ECEEF4;
padding: 20px;
background-color: #eceef4;
padding-top: 20px;
padding-bottom: 20px;
padding-right: 20px;
`;
export const App = () => {
@ -24,6 +27,7 @@ export const App = () => {
<Topbar handleTransfer={handleTransfer} />
<Transfer isOpen={isOpenTransfer} handleClose={handleTransfer} />
<Wrapper>
{/* <Admin/> */}
<Scheduler />
<Rightbar />
</Wrapper>

View File

@ -1,17 +1,18 @@
import React, { useState, useContext, MouseEvent } from 'react';
import React, { useState, useContext } from 'react';
import Collapse from '@material-ui/core/Collapse';
import { ReactComponent as Expand } from '../assets/expand.svg';
import { Course, Group } from '../types/index';
import { Course, Group, GroupType } from '../types/index';
import { coursesContext } from '../contexts/CoursesProvider';
import styled from 'styled-components';
import styled, { css } from 'styled-components';
import { makeStyles } from '@material-ui/core/styles';
import { ReactComponent as Bin } from '../assets/bin.svg';
import DeleteIcon from '@material-ui/icons/Delete';
import { useMemo } from 'react';
const CourseCardWrapper = styled.div`
position: relative;
display: flex;
min-height: 40px;
background-color: rgb(100, 181, 246);
background-color: #b5d2e0;
align-items: center;
justify-content: center;
flex-direction: column;
@ -22,19 +23,18 @@ const CourseCardWrapper = styled.div`
box-shadow: 9px 9px 8px -2px rgba(0, 0, 0, 0.59);
`;
const TitleWrapper = styled.div`
font-size: 14px;
font-weight: 550;
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px;
`
padding: 10px 10px 10px 2px;
`;
const BinIcon = styled(Bin)`
width: 20px;
height: 20px;
max-width: 20px;
min-width: 20px;
const BinIcon = styled(DeleteIcon)`
max-width: 30px;
min-width: 30px;
cursor: pointer;
&:hover {
fill: white;
@ -51,37 +51,63 @@ const CourseName = styled.div`
const ClassGroupStyled = styled.div`
position: relative;
padding-top: 1px;
padding-bottom: 1px;
padding-bottom: 5px;
:hover {
cursor: pointer;
background-color: #9ed3ff;
}
:last-child {
border-radius: 0 0 10px 10px;
}
`;
interface ExpandIconProps {
isSelected: boolean;
selected: boolean;
}
const ExpandIcon = styled(Expand) <ExpandIconProps>`
export const ExpandIcon = styled(Expand)<ExpandIconProps>`
width: 20px;
height: 20px;
max-width: 20px;
min-width: 20px;
transition: 0.2s;
transform: ${({ isSelected }) => (isSelected ? 'scaleY(-1);' : 'scaleY(1);')};
transform: ${({ selected }) => (selected ? 'scaleY(-1);' : 'scaleY(1);')};
`;
const TypeClass = styled.div`
type StyledGroupTypeProps = {
groupType: GroupType;
};
const StyledGroupType = styled.div<StyledGroupTypeProps>`
font-size: 12px;
position: absolute;
border-radius: 15px;
background-color: #00506b;
border: 2px solid;
background-color: ${({ groupType }) => (groupType === 'CLASS' ? '#FFDC61' : '#9ed3ff')};
border: 2px solid white;
min-width: 45px;
top: 5px;
left: 5px;
color: white;
font-weight: bold;
color: black;
`;
const FlexboxWrapper = styled.div`
display: flex;
flex-direction: column;
`;
type FlexItemProps = {
justifyContent?: string;
};
const FlexItem = styled.div<FlexItemProps>`
display: flex;
font-size: 14px;
font-weight: 600;
${({ justifyContent }) =>
justifyContent &&
css`
justify-content: ${justifyContent};
`}
`;
const useStyles = makeStyles({
@ -89,51 +115,100 @@ const useStyles = makeStyles({
maxHeight: '244px',
overflowY: 'auto',
'&::-webkit-scrollbar': {
width: '0.4em',
width: '0.3em',
borderStyle: 'none',
},
'&::-webkit-scrollbar-track': {
'-webkit-box-shadow': 'inset 0 0 6px rgba(1,0,0,0.1)',
borderRadius: '10px',
},
'&::-webkit-scrollbar-thumb': {
borderRadius: '10px',
backgroundColor: '#d4b851',
outline: '1px solid slategrey',
backgroundColor: '#4b4b4b',
},
},
});
interface CourseCardProps {
course: Course;
}
export const CourseCard = ({ course }: CourseCardProps) => {
const classes = useStyles();
const { addGroup, deleteFromBasket } = useContext(coursesContext)!;
const {
hoveredGroup,
changeGroupInBasket,
deleteFromBasket,
selectBasketCourseGroups,
changeHoveredGroup,
} = useContext(coursesContext)!;
const [isSelected, setSelected] = useState(false);
const groups = course.lectures === undefined ? course.classes : [...course.lectures, ...course.classes];
const onGroupClick = (group: Group, id: number) => addGroup(group, id);
const groups = [...course.lectures!, ...course.classes!];
const basketCourseGroups = useMemo(() => selectBasketCourseGroups(course.id), []);
const [previous, setPrevious] = useState(basketCourseGroups);
// console.log('lecture is: ', courseLecture);
// console.log('class is: ', courseClasses);
const onGroupClick = (group: Group, courseId: number) => {
setPrevious((prev) => (group.type === GroupType.CLASS ? { ...prev, classes: group } : { ...prev, lecture: group }));
changeGroupInBasket(group, courseId);
};
return (
<CourseCardWrapper>
<TitleWrapper>
<BinIcon onClick={() => deleteFromBasket(course.id)}></BinIcon>
<TitleWrapper onClick={() => setSelected(!isSelected)}>
<BinIcon
onClick={(e) => {
e.stopPropagation();
deleteFromBasket(course.id);
setSelected(false);
}}
></BinIcon>
<CourseName onClick={() => setSelected(!isSelected)}>{course.name}</CourseName>
<ExpandIcon onClick={() => setSelected(!isSelected)} isSelected={isSelected} />
<ExpandIcon onClick={() => setSelected(!isSelected)} selected={isSelected} />
</TitleWrapper>
<Collapse className={classes.expanded} in={isSelected} timeout="auto" unmountOnExit>
{groups
.sort((a, b) => b.type.localeCompare(a.type))
.map((group, index) => (
<ClassGroupStyled key={index} onClick={() => onGroupClick(group, course.id)}>
<TypeClass>{group.type === 'CLASS' ? 'Ćw.' : 'Wyk.'}</TypeClass>
<p>
{group.time} {group.room} <br></br> {group.lecturer}
</p>
</ClassGroupStyled>
))}
{groups.map((group: Group, index) => (
<ClassGroupStyled
key={index}
onClick={() => onGroupClick(group, course.id)}
onMouseEnter={() => {
if (group.type === GroupType.CLASS) {
changeGroupInBasket(group, course.id);
// setTimeout(()=> { changeHoveredGroup(courseClasses)},[500])
}
if (group.type === GroupType.LECTURE) {
changeGroupInBasket(group, course.id);
// setTimeout(()=> { changeHoveredGroup(courseLecture)},[500])
}
}}
onMouseLeave={() => {
if (hoveredGroup) {
if (hoveredGroup.type === GroupType.CLASS && previous.classes !== undefined) {
changeGroupInBasket(previous.classes, course.id);
}
if (hoveredGroup.type === GroupType.LECTURE && previous.lecture !== undefined) {
changeGroupInBasket(previous.lecture, course.id);
}
changeHoveredGroup(null);
}
}}
>
<StyledGroupType groupType={group.type}>{group.type === 'CLASS' ? 'ĆW' : 'WYK'}</StyledGroupType>
<FlexboxWrapper>
{group.lecturer.replace('UAM', '').length >= 32 ? (
<FlexItem style={{ justifyContent: 'center', marginLeft: '40px' }}>
{group.lecturer.replace('UAM', '')}
</FlexItem>
) : (
<FlexItem style={{ justifyContent: 'center', marginLeft: '10px' }}>
{group.lecturer.replace('UAM', '')}
</FlexItem>
)}
<FlexItem style={{ justifyContent: 'center', margin: '0 50px' }}>
<span>{/*group.time*/}</span> <span> Sala: {group.room}</span>
</FlexItem>
</FlexboxWrapper>
</ClassGroupStyled>
))}
</Collapse>
</CourseCardWrapper>
);

View File

@ -1,31 +1,30 @@
import React, { useState, useContext, useEffect, MouseEvent, forwardRef } from 'react';
import React, { useState, useContext, useEffect, MouseEvent, useMemo } from 'react';
import { coursesContext } from '../contexts/CoursesProvider';
import { Course } from '../types';
import styled from 'styled-components';
const DropdownContainer = styled.div`
position: relative;
z-index: 99999999;
max-height: 420px;
max-height: 396px;
border-radius: 3px;
overflow-y: auto;
opacity: 0.97;
box-shadow: 0.05em 0.2em 0.6em rgba(0, 0, 0, 0.2);
scroll-snap-type: y mandatory;
scroll-behavior: smooth;
::-webkit-scrollbar-track {
background-color: #f2f4f7;
border-radius: 10px;
background-color: #f5f5f5;
}
::-webkit-scrollbar {
width: 12px;
background-color: #f5f5f5;
background-color: #f2f4f7;
width: 5px;
border-style: none;
}
::-webkit-scrollbar-thumb {
border-radius: 10px;
background-color: black;
border: 1px solid;
background-color: #4b4b4b;
}
`;
@ -33,7 +32,7 @@ const CourseContainer = styled.div`
padding: 5px;
padding-left: 20px;
background-color: #f2f4f7;
font-size: 18px;
font-size: 16px;
font-weight: 500;
scroll-snap-align: end;
:hover {
@ -48,27 +47,22 @@ interface DropdownProps {
handleCloseDropdown: () => void;
}
export const Dropdown = forwardRef(({ open, input, handleCloseDropdown }: DropdownProps, ref: any) => {
//courses - choosenCourses
export const Dropdown = ({ open, input, handleCloseDropdown }: DropdownProps) => {
const { courses, selectBasketNames, addCourseToBasket } = useContext(coursesContext)!;
const basketNames = useMemo(() => selectBasketNames(), [selectBasketNames]);
const [filteredCourses, setFilteredCourses] = useState<Array<Course>>([]);
const { courses, basket, addToBasket } = useContext(coursesContext)!;
useEffect(() => {
console.log('wut');
}, [open, input, handleCloseDropdown]);
useEffect(() => {
console.log('input is: ', input);
}, [input]);
useEffect(() => {
console.log('is open: ', open);
}, [open]);
const onCourseClick = (event: MouseEvent) => {
const target = event.currentTarget;
if (target.id && target.textContent) {
const course = filteredCourses.find(({ id }) => id.toString() === target.id)!;
addCourseToBasket(course);
handleCloseDropdown();
}
};
useEffect(() => {
const filterCourses = (input: string) => {
const choosenCoursesNames = basket.map(({ name }) => name.trim());
const filteredCourses = courses.filter(
({ name }) =>
name
@ -80,24 +74,12 @@ export const Dropdown = forwardRef(({ open, input, handleCloseDropdown }: Dropdo
.toLowerCase()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, ''),
) && !choosenCoursesNames.includes(name),
) && !basketNames.includes(name),
);
setFilteredCourses(filteredCourses);
};
console.log("filtering courses");
filterCourses(input);
}, [open, input, basket]);
const onCourseClick = async (event: MouseEvent) => {
const target = event.currentTarget;
if (target.id && target.textContent) {
const course = filteredCourses.find(({ id }) => id.toString() === target.id)!;
console.log('added course is');
console.log(course);
addToBasket(course);
handleCloseDropdown();
}
};
}, [basketNames, courses, input]);
return (
<DropdownContainer>
@ -112,4 +94,4 @@ export const Dropdown = forwardRef(({ open, input, handleCloseDropdown }: Dropdo
)}
</DropdownContainer>
);
});
};

View File

@ -12,7 +12,7 @@ export const Profile = ({ anchorEl, handleClose }: ProfileProps) => {
return (
<Menu anchorEl={anchorEl} keepMounted open={Boolean(anchorEl)} onClose={handleClose}>
<MenuItem>Profil</MenuItem>
{/* <MenuItem>Profil</MenuItem> */}
<MenuItem onClick={logout}>Wyloguj</MenuItem>
</Menu>
);

View File

@ -2,28 +2,24 @@ import React, { useContext } from 'react';
import { CourseCard } from './CourseCard';
import { coursesContext } from '../contexts/CoursesProvider';
import styled from 'styled-components';
import { debounce } from 'lodash';
import { debounce } from '../utils/index';
const RightbarStyled = styled.div`
padding-top: 10px;
padding-left: 15px;
padding-right: 15px;
const RightbarWrapper = styled.div`
padding: 15px;
text-align: center;
height: 100%;
width: 300px;
width: 350px;
overflow-y: scroll;
::-webkit-scrollbar-track {
border-radius: 10px;
background-color: #f5f5f5;
}
::-webkit-scrollbar {
width: 12px;
background-color: #f5f5f5;
width: 5px;
border-style: none;
}
::-webkit-scrollbar-thumb {
border-radius: 10px;
background-color: black;
border: 1px solid;
background-color: #4b4b4b;
}
background-color: white;
border-radius: 5px;
@ -34,36 +30,34 @@ const SaveButton = styled.div`
justify-content: center;
align-items: center;
user-select: none;
background-color: #417cab;
background-color: #43a047;
border-radius: 10px;
cursor: pointer;
height: 40px;
margin-bottom: 10px;
&:hover {
color: white;
color: #ffffff;
box-shadow: 0px 5px 4px 0px rgba(0, 0, 0, 0.24);
}
box-shadow: 6px 6px 6px -2px rgba(0, 0, 0, 0.59);
&:active {
background-color: #54c457;
}
box-shadow: 0px 3px 3px 0px rgba(0, 0, 0, 0.24);
`;
export const Rightbar = () => {
const { courses, basket, saveBasket } = useContext(coursesContext)!;
const getBasketGroups = () => {
const names = basket.map(({ name }) => name);
return courses.filter(({ name }) => names.includes(name));
};
const filteredCourses = getBasketGroups();
const { selectBasketCourses, saveBasket } = useContext(coursesContext)!;
const basketCourses = selectBasketCourses();
const handleSave = debounce(() => saveBasket(), 500);
//need to insert student name from db and course maybe based on current time or from db too
return (
<RightbarStyled>
<RightbarWrapper>
<SaveButton onClick={handleSave}>ZAPISZ</SaveButton>
{filteredCourses.map((course, index) => (
<CourseCard course={course} key={index} />
{basketCourses.map((course) => (
<CourseCard course={course} key={course.id} />
))}
</RightbarStyled>
</RightbarWrapper>
);
};

View File

@ -1,4 +1,4 @@
import React, { useEffect, MouseEvent, useRef, useCallback, useLayoutEffect } from 'react';
import React, { useEffect, useRef } from 'react';
import { useState } from 'react';
import { SchedulerEvents } from './SchedulerEvents';
import { days, hours } from '../constants/index';
@ -11,6 +11,7 @@ const SchedulerWrapper = styled.div`
padding: 10px 40px 25px 10px;
border-radius: 5px;
margin-right: 20px;
margin-left: 20px;
flex-direction: column;
justify-content: center;
align-items: center;
@ -42,9 +43,9 @@ interface TableCellProps {
}
const TableCell = styled.div<TableCellProps>`
border-width: ${({ isHourColumn }) => !isHourColumn && '2px'};
border-width: ${({ isHourColumn }) => !isHourColumn && '1px'};
border-style: ${({ isHourColumn }) => !isHourColumn && 'none solid dotted none'};
border-color: rgb(242, 243, 245);
border-color: rgb(235, 235, 235);
display: flex;
align-items: center;
justify-content: ${({ isHourColumn }) => (isHourColumn ? 'flex-end' : 'center')};
@ -55,7 +56,7 @@ const TableCell = styled.div<TableCellProps>`
user-select: none;
border-collapse: collapse;
:nth-child(2) {
border-left: 2px solid rgb(242, 243, 245);
border-left: 1px solid rgb(235, 235, 235);
}
font-weight: bold;
`;
@ -63,18 +64,13 @@ const TableCell = styled.div<TableCellProps>`
export const Scheduler = () => {
const cellRef = useRef<HTMLDivElement>(null);
const [cellWidth, setCellWidth] = useState(0);
const [cellTop, setCellTop] = useState(0);
const [cellHeight, setCellHeight] = useState(0);
console.log('cell height: ', cellHeight);
useEffect(() => {
const handleResize = () => {
if (cellRef.current) {
setCellWidth(cellRef.current.getBoundingClientRect().width);
setCellTop(cellRef.current.getBoundingClientRect().top);
setCellHeight(cellRef.current.getBoundingClientRect().height);
cellRef.current.style.backgroundColor = 'blue';
}
};
handleResize();
@ -83,52 +79,52 @@ export const Scheduler = () => {
}, []);
return (
<>
<SchedulerWrapper>
<TableHead>
{days.map((day, indexCell) =>
indexCell === 0 ? (
<TableCell isHourColumn={true} key={indexCell}>
{day}
</TableCell>
) : (
<TableCell style={{ borderStyle: 'none none solid none' }} key={indexCell}>
{day}
</TableCell>
),
)}
</TableHead>
<TableBody>
{hours.map((hour, indexRow) => (
<TableRow key={indexRow}>
{[hour, '', '', '', '', ''].map((value, indexCell) =>
indexCell === 0 ? (
<TableCell isHourColumn={true} cellHeight={cellHeight} key={`${indexRow}${indexCell}`}>
{value}
</TableCell>
) : indexRow === 0 && indexCell === 1 ? (
<TableCell ref={cellRef} key={`${indexRow}${indexCell}`}>
{value}
</TableCell>
) : indexRow === 23 ? (
<TableCell style={{ borderBottom: '2px solid rgb(242, 243, 245)' }} key={`${indexRow}${indexCell}`}>
{value}
</TableCell>
) : indexCell === 5 ? (
<TableCell key={`${indexRow}${indexCell}`}>{value}</TableCell>
) : indexRow % 2 !== 0 ? (
<TableCell style={{ borderBottom: '2px solid rgb(242, 243, 245)' }} key={`${indexRow}${indexCell}`}>
{value}
</TableCell>
) : (
<TableCell key={`${indexRow}${indexCell}`}>{value}</TableCell>
),
)}
</TableRow>
))}
<SchedulerEvents cellTop={cellTop} cellWidth={cellWidth} cellHeight={cellHeight} />
</TableBody>
</SchedulerWrapper>
</>
<SchedulerWrapper>
<TableHead>
{days.map((day, indexCell) =>
indexCell === 0 ? (
<TableCell isHourColumn={true} key={indexCell}>
{day}
</TableCell>
) : (
<TableCell style={{ borderStyle: 'none none solid none' }} key={indexCell}>
{day}
</TableCell>
),
)}
</TableHead>
<TableBody>
{hours.map((hour, indexRow) => (
<TableRow key={indexRow}>
{[hour, '', '', '', '', ''].map((value, indexCell) =>
indexCell === 0 ? (
<TableCell isHourColumn={true} cellHeight={cellHeight} key={`${indexRow}${indexCell}`}>
{value}
</TableCell>
) : indexRow === 0 && indexCell === 1 ? (
<TableCell ref={cellRef} key={`${indexRow}${indexCell}`}>
{value}
</TableCell>
) : indexRow === 23 ? (
<TableCell style={{ borderBottom: '1px solid rgb(235, 235, 235)' }} key={`${indexRow}${indexCell}`}>
{value}
</TableCell>
) : indexRow === 5 ? (
<TableCell style={{ borderBottom: '1px solid rgb(235, 235, 235)' }} key={`${indexRow}${indexCell}`}>
{value}
</TableCell>
) : indexRow % 2 !== 0 ? (
<TableCell style={{ borderBottom: '1px solid rgb(235, 235, 235)' }} key={`${indexRow}${indexCell}`}>
{value}
</TableCell>
) : (
<TableCell key={`${indexRow}${indexCell}`}>{value}</TableCell>
),
)}
</TableRow>
))}
<SchedulerEvents cellWidth={cellWidth} cellHeight={cellHeight} />
</TableBody>
</SchedulerWrapper>
);
};

View File

@ -1,57 +1,26 @@
import React, { useContext, useEffect, useState, MouseEvent } from 'react';
import React, { useContext } from 'react';
import { SchedulerRow } from './SchedulerRow';
import { coursesContext } from '../contexts/CoursesProvider';
import { Group, Basket } from '../types';
import { selectGroupsToShow } from '../utils/index';
import { ROWS_COUNT } from '../constants';
interface SchedulerEventsProps {
cellTop: number;
cellWidth: number;
cellHeight: number;
}
export const SchedulerEvents = ({ cellTop, cellWidth, cellHeight }: SchedulerEventsProps) => {
const { basket } = useContext(coursesContext)!;
console.log(`values: cellTop: ${cellTop}, cellWidth: ${cellWidth}, cellHeight: ${cellHeight}`);
const [choosenGroupsMappedToEvents, setChoosenGroupsMappedToEvents] = useState<any>([]);
export const SchedulerEvents = ({ cellWidth, cellHeight }: SchedulerEventsProps) => {
const { selectSchedulerEvents } = useContext(coursesContext)!;
const groupTimeToEventRowMapping: { [time: string]: number } = {
'8.15': 0,
'10.00': 1,
'11.45': 2,
'13.45': 3,
'15.30': 4,
'17.15': 5,
};
useEffect(() => {
function mapGroupTimeToEventRow(basket: Array<Basket>) {
const classes = basket.map(({ classes, name }) => ({ ...classes, name })) as Array<Group & { name: string }>;
const lectures = basket.map(({ lecture, name }) => ({ ...lecture, name })) as Array<Group & { name: string }>;
const merged = [...classes, ...lectures];
//deleted if statement, maybe it is needed
const groupsMapped = merged.map(({ id, day, lecturer, room, time, name, type }) => ({
id,
day,
lecturer,
room,
eventRow: groupTimeToEventRowMapping[time],
name,
type,
}));
setChoosenGroupsMappedToEvents(groupsMapped);
}
mapGroupTimeToEventRow(basket);
}, [basket]);
const schedulerEvents = selectSchedulerEvents();
return (
<div>
{[...Array(6)].map((_, index) => (
{[...Array(ROWS_COUNT)].map((_, index) => (
<SchedulerRow
key={index}
groups={choosenGroupsMappedToEvents.filter((group: any) => group.eventRow === index)}
groups={selectGroupsToShow(schedulerEvents, index)}
indexRow={index}
cellTop={
rowTop={
index === 0
? cellHeight / 2
: index === 1

View File

@ -1,92 +1,163 @@
import React, { MouseEvent, useState } from 'react';
import { Group, GroupType } from '../types';
import styled from 'styled-components/macro';
import React, { Fragment, MouseEvent, useState, useEffect, useContext } from 'react';
import { GroupType, SchedulerEvent } from '../types';
import styled, { css } from 'styled-components/macro';
import Popover from '@material-ui/core/Popover';
import Typography from '@material-ui/core/Typography';
import { makeStyles, createStyles, Theme } from '@material-ui/core/styles';
import { MONDAY_TO_FRIDAY } from '../constants';
import { coursesContext } from '../contexts/CoursesProvider';
const useStyles = makeStyles((theme: Theme) =>
createStyles({
popover: {
pointerEvents: 'none',
fontSize: '14px',
},
paper: {
padding: theme.spacing(1),
marginLeft: 5,
textAlign: 'center',
padding: '15px 15px 15px 15px',
textAlign: 'left',
lineHeight: `1 !important`,
},
}),
);
interface ClassesWrapperProps {
const PopoverSpan = styled.span`
font-weight: bold;
margin-right: 2px;
`;
interface SchedulerEventsWrapperProps {
eventIndex: number;
cellTop: number;
rowTop: number;
cellWidth: number;
cellHeight: number;
}
const ClassesWrapper = styled.div<ClassesWrapperProps>`
const SchedulerEventsWrapper = styled.div<SchedulerEventsWrapperProps>`
position: absolute;
display: flex;
top: ${({ cellTop }) => cellTop}px;
left: ${({ cellWidth, eventIndex }) => (cellWidth * 1) / 5 + 15 + cellWidth * eventIndex}px;
top: ${({ rowTop }) => rowTop}px;
left: ${({ cellWidth, eventIndex }) => (cellWidth * 1) / 5 + 4 + cellWidth * eventIndex}px;
width: ${({ cellWidth }) => cellWidth - 10}px;
height: ${({ cellHeight }) => cellHeight * 3}px;
z-index: 2;
padding-left: 10px;
`;
interface ClassesProps {
interface SchedulerEventProps {
cellWidth: number;
cellHeight: number;
groupType: GroupType;
isHovered: boolean;
}
const Classes = styled.div<ClassesProps>`
const StyledSchedulerEvent = styled.div<SchedulerEventProps>`
display: flex;
flex: 1;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 2;
z-index: 20000;
font-size: 0.65vw;
line-height: normal;
border-radius: 10px;
height: ${({ cellHeight }) => cellHeight * 3}px;
width: ${({ cellWidth }) => (cellWidth * 3) / 4}px;
margin-right: 5px;
text-align: left;
background-color: ${({ groupType }) => (groupType === 'CLASS' ? '#FFDC61' : '#9ed3ff')};
box-shadow: 9px 9px 8px -2px rgba(0, 0, 0, 0.59);
padding: 5px 5px 0 5px;
text-align: center;
background-color: ${({ groupType, isHovered }) => {
if (isHovered) {
return groupType === 'CLASS' ? '#ffefb5' : '#d4ecff';
} else {
return groupType === 'CLASS' ? '#FFDC61' : '#9ed3ff';
}
}};
box-shadow: 3px 3px 3px 0px rgba(0, 0, 0, 0.75);
`;
const threeStyles = () => {
return css`
white-space: nowrap;
text-overflow: ellipsis;
max-width: 70px;
`;
};
type BoldParagraphProps = {
isThree?: boolean;
};
const BoldParagraph = styled.p<BoldParagraphProps>`
overflow: hidden;
flex: 3;
${({ isThree }) => isThree && threeStyles}
`;
const ClassWrap = styled.div`
font-weight: 700;
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
line-height: normal;
`;
const TextWrapper = styled.div`
flex: 1;
width: inherit;
padding: 0 3px 5px 3px;
display: flex;
justify-content: space-between;
`;
interface SchedulerRowProps {
groups: Array<Group & { name: string }>;
groups: Array<SchedulerEvent>;
indexRow: number;
cellTop: number;
rowTop: number;
cellWidth: number;
cellHeight: number;
}
export const SchedulerRow = ({ groups, indexRow, cellTop, cellWidth, cellHeight }: SchedulerRowProps) => {
const getGroupsPerDay = (groups: Array<SchedulerEvent>) => {
const groupsPerDay: any = { 0: 0, 1: 0, 2: 0, 3: 0, 4: 0 };
for (const group of groups) {
groupsPerDay[group.day]++;
}
return groupsPerDay;
};
export const SchedulerRow = ({ groups, indexRow, rowTop, cellWidth, cellHeight }: SchedulerRowProps) => {
const { hoveredGroup } = useContext(coursesContext)!;
const classes = useStyles();
const groupsPerDay = getGroupsPerDay(groups);
const [anchorEl, setAnchorEl] = React.useState<HTMLDivElement | null>(null);
const [popoverId, setPopoverId] = useState<string | null>(null);
//looks weird
const handlePopoverOpen = (event: MouseEvent<HTMLDivElement, globalThis.MouseEvent>) => {
console.log('I was clicked!!!!');
setAnchorEl(event.currentTarget);
setPopoverId(event.currentTarget.id);
};
const handlePopoverClose = () => {
setAnchorEl(null);
const handlePopoverClose = (e: MouseEvent<any>) => {
console.log('current target:', e.currentTarget);
console.log(' target:', e.target);
setPopoverId(null);
setAnchorEl(null);
console.log('click awayyy');
};
useEffect(() => {
console.log('anchorEl: ', anchorEl);
}, [anchorEl]);
const open = Boolean(anchorEl);
const id = open ? 'simple-popover' : undefined;
return (
<div>
{[...Array(5)].map((_, eventIndex) => (
<ClassesWrapper
{[...Array(MONDAY_TO_FRIDAY)].map((_, eventIndex) => (
<SchedulerEventsWrapper
eventIndex={eventIndex}
cellTop={cellTop}
rowTop={rowTop}
cellWidth={cellWidth}
cellHeight={cellHeight}
key={eventIndex}
@ -95,27 +166,35 @@ export const SchedulerRow = ({ groups, indexRow, cellTop, cellWidth, cellHeight
{groups.map(
(group, index) =>
group.day === eventIndex && (
<>
<Classes
onClick={() => {
console.log('group: ', group);
}}
<Fragment key={index}>
<StyledSchedulerEvent
aria-describedby={id}
isHovered={group.id === hoveredGroup?.id}
groupType={group.type}
cellWidth={cellWidth}
cellHeight={cellHeight}
id={`eventRow${indexRow}eventCol${eventIndex}${index}`}
key={index}
aria-owns={open ? `mouse-over-popover` : undefined}
aria-haspopup="true"
onMouseEnter={(e) => handlePopoverOpen(e)}
onMouseLeave={handlePopoverClose}
onClick={(e) => handlePopoverOpen(e)}
>
<div>
<p style={{ fontWeight: 700 }}>{groups[index].name}</p>
<p>{groups[index].room}</p>
</div>
</Classes>
<ClassWrap>
<BoldParagraph isThree={groupsPerDay[group.day] >= 3}>{groups[index].name}</BoldParagraph>
{groupsPerDay[group.day] < 3 ? (
<TextWrapper>
<div>{`${groups[index].time[0]}-${groups[index].time[1]}`}</div>
<div>3/{groups[index].capacity}</div>
</TextWrapper>
) : (
<TextWrapper style={{ flexDirection: 'column' }}>
<div style={{ alignSelf: 'flex-end' }}>3/{groups[index].capacity}</div>
</TextWrapper>
)}
</ClassWrap>
</StyledSchedulerEvent>
<Popover
id={`mouse-over-popover`}
id={id}
className={classes.popover}
classes={{
paper: classes.paper,
@ -133,16 +212,34 @@ export const SchedulerRow = ({ groups, indexRow, cellTop, cellWidth, cellHeight
onClose={handlePopoverClose}
disableRestoreFocus
>
<Typography>
<p>{groups[index].name}</p>
<p>{groups[index].lecturer}</p>
<p>{groups[index].room}</p>
</Typography>
<div
style={{ display: 'flex', flexDirection: 'column', zIndex: 20000 }}
onClick={() => {
console.log('XDD');
}}
>
<p style={{ margin: '7px 0 7px 0', fontWeight: 'bold' }}>{groups[index].name}</p>
<p style={{ margin: '2px 0 2px 0' }}>
<PopoverSpan>Prowadzący:</PopoverSpan> {groups[index].lecturer}
</p>
<p style={{ margin: '2px 0 2px 0' }}>
<PopoverSpan>Sala zajęć</PopoverSpan>: {groups[index].room}
</p>
<p style={{ margin: '2px 0 2px 0' }}>
<PopoverSpan>Kod przedmiotu: </PopoverSpan>ACB129
</p>
<p style={{ margin: '2px 0 2px 0' }}>
<PopoverSpan>Kod grupy: </PopoverSpan>FVJ753
</p>
<p style={{ margin: '2px 0 2px 0' }}>
<PopoverSpan>Punkty ECTS:</PopoverSpan> 2
</p>
</div>
</Popover>
</>
</Fragment>
),
)}
</ClassesWrapper>
</SchedulerEventsWrapper>
))}
</div>
);

View File

@ -0,0 +1,109 @@
import { ClickAwayListener } from '@material-ui/core';
import React, { useState } from 'react';
import styled from 'styled-components';
import { ExpandIcon } from './CourseCard';
const Wrapper = styled.div`
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
margin-top: 15px;
min-width:130px;
font-family: 'Roboto', sans-serif;
background-color: #f1f2f5;
color: black;
border-top-left-radius: 6px;
border-bottom-left-radius: 6px;
&:focus {
outline: none;
}
padding-left: 15px;
cursor: pointer;
border-top-left-radius: 6px;
border-bottom-left-radius: 6px;
user-select: none;
`;
const Header = styled.div`
display: flex;
width:100%;
align-items: center;
justify-content: space-between;
`;
const HeaderTitle = styled.div`
font-family: 'Roboto', sans-serif;
font-size: 16px;
font-weight: 400;
`;
const List = styled.ul`
position: absolute;
top: 50px;
list-style-type: none;
margin: 0;
padding: 0;
margin-left: -15px;
background-color: #f2f4f7;
`;
const ListItem = styled.li`
font-family: 'Roboto', sans-serif;
font-size: 16px;
user-select: none;
padding-top: 10px;
padding-bottom: 10px;
padding-left: 15px;
padding-right: 37px;
font-weight: 400;
:hover {
background-color: #eceef4;
}
:first-child{
margin-top:10px;
}
`;
const ExpandIconSelect = styled(ExpandIcon)`
width: 10px;
height: 10px;
`;
export const SelectMenu = () => {
const [isOpen, setIsOpen] = useState(false);
const [selectedOption, setSelectedOption] = useState('przedmioty');
return (
<ClickAwayListener
onClickAway={() => {
setIsOpen(false);
}}
>
<Wrapper>
<Header
onClick={() => {
console.log('clicked');
setIsOpen(!isOpen);
}}
>
<HeaderTitle>{selectedOption}</HeaderTitle>
<ExpandIconSelect selected={isOpen} />
</Header>
{isOpen && (
<List>
<ListItem
onClick={() => {
setSelectedOption('przedmioty');
setIsOpen(false);
}}
>
przedmioty
</ListItem>
<ListItem
onClick={() => {
setSelectedOption('studenci');
setIsOpen(false);
}}
>
studenci
</ListItem>
</List>
)}
</Wrapper>
</ClickAwayListener>
);
};

View File

@ -1,6 +1,4 @@
import React, { useState, MouseEvent, ChangeEvent, useEffect } from 'react';
import Transfer from '../assets/transfer.png';
import Search from '../assets/search.svg';
import React, { useState, MouseEvent, ChangeEvent, useEffect, useCallback } from 'react';
import { ReactComponent as Close } from '../assets/close.svg';
import ProfileIcon from '../assets/account.svg';
import { Profile } from './Profile';
@ -9,9 +7,10 @@ import PolishIcon from '../assets/poland.svg';
import EnglishIcon from '../assets/united-kingdom.svg';
import styled from 'styled-components/macro';
import ClickAwayListener from 'react-click-away-listener';
import { SelectMenu } from './SelectMenu';
const Topbar = styled.div`
background-color: #E3E5ED;
background-color: #e3e5ed;
height: 80px;
padding: 5px;
font-size: 24px;
@ -31,19 +30,22 @@ const LogoWrapper = styled.div`
const Logo = styled.img`
width: 70px;
height: 70px;
@media only screen and (max-width: 670px) {
width: 60px;
height: 60px;
@media only screen and (max-width: 1533px) {
flex: auto;
}
`;
const Text = styled.div`
margin-left: 10px;
font-size: 1.4rem;
user-select: none;
@media only screen and (max-width: 670px) {
margin-left: 10px;
font-size: 1.4rem;
user-select: none;
@media only screen and (max-width: 1533px) {
display: none;
}
@media only screen and (max-width: 1828px) {
margin-right: 10px;
text-align: center;
}
`;
const FlexboxColumn = styled.div`
@ -53,28 +55,37 @@ const FlexboxColumn = styled.div`
`;
const InputWrapper = styled.div`
width: 100%;
display: flex;
margin-top: 15px;
max-height: 40px;
background-color: #f2f4f7;
border-radius: 6px;
align-items: center;
border-radius: 6px 6px 6px 6px;
padding-left: 6px;
&:hover {
background-color: #ffffff;
}
&:hover > input {
background-color: #ffffff;
}
`;
const Input = styled.input`
font-family: 'Roboto', sans-serif;
font-size: 18px;
background-color: #f1f2f5;
font-size: 20px;
height: 40px;
max-height: 40px;
width: 100%;
border: none;
margin-left: 5px;
border-top-left-radius: 6px;
border-bottom-left-radius: 6px;
&:focus {
outline: none;
}
`;
const CloseIcon = styled(Close)`
align-self: center;
width: 30px;
height: 30px;
margin-right: 5px;
@ -103,9 +114,9 @@ const Icon = styled.img`
}
`;
export const Flexbox = styled.div`
display: flex;
`;
interface TopbarProps {
handleTransfer: (e: MouseEvent) => void;
@ -122,21 +133,22 @@ export default function ({ handleTransfer }: TopbarProps) {
const handleProfile = (event: MouseEvent<HTMLImageElement>) => setAnchorEl(event.currentTarget);
const handleClose = () => setAnchorEl(null);
const handleCloseProfile = () => setAnchorEl(null);
const handleClearInput = () => setClearInput(!clearInput);
const handleClearInput = useCallback(() => setClearInput((clearInput) => !clearInput), []);
const handleChange = (event: ChangeEvent<HTMLInputElement>) => setInput(event.target.value);
const handleClick = () => setOpen(true);
const handleShowDropdown = () => setOpen(true);
const handleCloseDropdown = () => setOpen(false);
const handleClickAway = () => setOpen(false);
useEffect(() => {
clearInput && (setInput(''), handleClearInput());
}, [clearInput]);
if (clearInput) {
setInput('');
handleClearInput();
}
}, [clearInput, handleClearInput]);
return (
<Topbar>
@ -145,11 +157,26 @@ export default function ({ handleTransfer }: TopbarProps) {
<Text> plan na plan </Text>
</LogoWrapper>
<FlexboxColumn>
<ClickAwayListener onClickAway={handleClickAway}>
<InputWrapper>
<Input placeholder="Wyszukaj przedmiot..." onChange={handleChange} onClick={handleClick} value={input} />
<CloseIcon onClick={handleClearInput} />
</InputWrapper>
<ClickAwayListener onClickAway={handleCloseDropdown}>
<Flexbox>
{/* <SelectMenu /> */}
<InputWrapper>
{/* <SelectSearch value={value} onChange={Change}>
<SelectOption value="przedmiot">Przedmiot</SelectOption>
<SelectOption value="student">Student</SelectOption>
</SelectSearch> */}
<Input
placeholder={`Wyszukaj...`}
onChange={handleChange}
value={input}
onFocus={() => {
handleShowDropdown();
}}
/>
<CloseIcon onClick={handleClearInput} />
</InputWrapper>
</Flexbox>
<Dropdown open={open} input={input} handleCloseDropdown={handleCloseDropdown} />
</ClickAwayListener>
</FlexboxColumn>
@ -159,7 +186,7 @@ export default function ({ handleTransfer }: TopbarProps) {
{/* <Icon alt="transfer" src={Transfer} onClick={handleTransfer} /> */}
<Icon alt="change_language" src={isPolish ? EnglishIcon : PolishIcon} onClick={onLangChange} />
<Icon alt="profile" src={ProfileIcon} onClick={handleProfile} />
<Profile anchorEl={anchorEl} handleClose={handleClose} />
<Profile anchorEl={anchorEl} handleClose={handleCloseProfile} />
</IconWrapper>
</Topbar>
);

View File

@ -1,37 +1,44 @@
export const days = [
"",
"Poniedziałek",
"Wtorek",
"Środa",
"Czwartek",
"Piątek",
];
export const days = ['', 'Poniedziałek', 'Wtorek', 'Środa', 'Czwartek', 'Piątek'];
export const hours = [
"8:00",
"",
"9:00",
"",
"10:00",
"",
"11:00",
"",
"12:00",
"",
"13:00",
"",
"14:00",
"",
"15:00",
"",
"16:00",
"",
"17:00",
"",
"18:00",
"",
"19:00",
"",
'8:00',
'',
'9:00',
'',
'10:00',
'',
'11:00',
'',
'12:00',
'',
'13:00',
'',
'14:00',
'',
'15:00',
'',
'16:00',
'',
'17:00',
'',
'18:00',
'',
'19:00',
'',
];
export const MONDAY_TO_FRIDAY = 5;
//added 12:00, one of lectures starts at that time
export const courseStartTimeToEventRow: { [time: string]: number } = {
'8.15': 0,
'10.00': 1,
'11.45': 2,
'12.00': 2,
'13.45': 3,
'15.30': 4,
'17.15': 5,
'18.45': 6,
};
//groupTimeToEventRowMapping - 1;
export const ROWS_COUNT = 6;

View File

@ -1,10 +1,11 @@
import React, { useState, useEffect, createContext, ReactNode } from 'react';
import { User } from '../types';
import axios from 'axios';
import { axiosInstance } from '../utils/axiosInstance';
export interface CASContext {
user?: User;
logout: () => void;
token?: string | null;
}
export const CASContext = createContext<CASContext | undefined>(undefined);
@ -15,29 +16,28 @@ export interface CASProviderProps {
export const CASProvider = ({ children }: CASProviderProps) => {
const [user, setUser] = useState<User | undefined>(undefined);
const [token, setToken] = useState<string | null>(null);
useEffect(() => {
const login = async () => {
const urlParams = new URLSearchParams(window.location.search);
const ticket = urlParams.get('ticket');
if (!ticket) {
redirectToCASLoginService();
}
try {
if (!sessionStorage.getItem('userToken')) {
const { data: token } = await axiosInstance.get(`${process.env.REACT_APP_API_URL}/token?ticket=${ticket}`);
sessionStorage.setItem('userToken', token);
}
const token = sessionStorage.getItem('userToken');
setToken(token);
} catch (e) {
console.log(e);
}
};
login();
}, []);
const login = async () => {
const urlParams = new URLSearchParams(window.location.search);
const ticket = urlParams.get('ticket');
if (!ticket) {
redirectToCASLoginService();
}
try {
if (!sessionStorage.getItem('userToken')) {
const { data: token } = await axios.get(`${process.env.REACT_APP_API_URL}/token?ticket=${ticket}`);
sessionStorage.setItem('userToken', token);
setUser({ ...user, token });
}
const token = sessionStorage.getItem('userToken');
setUser({ ...user, token });
} catch (e) {
console.log(e);
}
};
function logout() {
redirectToCASLogoutService();
}
@ -50,5 +50,5 @@ export const CASProvider = ({ children }: CASProviderProps) => {
window.location.replace(`https://cas.amu.edu.pl/cas/login?service=${window.origin}&locale=pl`);
}
return <CASContext.Provider value={{ user, logout }}>{children}</CASContext.Provider>;
return <CASContext.Provider value={{ user, token, logout }}>{children}</CASContext.Provider>;
};

View File

@ -1,16 +1,33 @@
import React, { useState, createContext, useEffect, ReactNode, useContext } from 'react';
import { Course, Group, Basket, GroupType } from '../types';
import axios from 'axios';
import { CASContext } from './CASProvider';
import React, { useState, createContext, useEffect, ReactNode } from 'react';
import { Course, Group, Basket, GroupType, SchedulerEvent } from '../types';
import { useSnackbar } from 'notistack';
import { createClassTime } from '../utils';
import { axiosInstance } from '../utils/axiosInstance';
import CloseIcon from '@material-ui/icons/Close';
import styled from 'styled-components';
const StyledCloseIcon = styled(CloseIcon)`
color: #000000;
&:hover {
color: white;
cursor: pointer;
}
`;
interface CourseContext {
courses: Array<Course>;
basket: Array<Basket>;
addToBasket: (courses: Course) => void;
addGroup: (group: Group, id: number) => void;
hoveredGroup: Group | undefined | null;
addCourseToBasket: (courses: Course) => void;
changeHoveredGroup: (group: Group | null) => void;
changeGroupInBasket: (group: Group, courseId: number) => void;
restoreGroupInBasket: (restoreGroup: Group, courseId: number) => void;
deleteFromBasket: (id: number) => void;
saveBasket: () => void;
selectSchedulerEvents: () => Array<SchedulerEvent>;
selectBasketNames: () => Array<string>;
selectBasketCourses: () => Array<Course>;
selectBasketCourseGroups: (courseId: number) => { lecture: Group | undefined; classes: Group | undefined };
}
export const coursesContext = createContext<CourseContext | undefined>(undefined);
@ -19,28 +36,60 @@ interface CoursesProviderProps {
}
export const CoursesProvider = ({ children }: CoursesProviderProps) => {
//fetch courses with groups
const [courses, setCourses] = useState<Array<Course>>([]);
const [basket, setBasket] = useState<Array<Basket>>([]);
const { enqueueSnackbar } = useSnackbar();
const { closeSnackbar } = useSnackbar();
const CAS = useContext(CASContext)!;
const token = CAS?.user?.token;
const selectBasketIds = (basket: Array<Basket>) => {
const classesIds = basket.map((course) => course.classes.id);
const lecturesIds = basket.map((course) => course?.lecture?.id);
return lecturesIds[0] === undefined ? classesIds : [...classesIds, ...lecturesIds];
//fetch courses with groups
const [courses, setCourses] = useState<Array<Course>>([]);
const [basket, setBasket] = useState<Array<Basket>>([]);
const [hoveredGroup, setHoveredGroup] = useState<Group | undefined | null>(null);
const selectBasketIds = () => {
const classesIds = basket.map((course) => course?.classes?.id).filter((course) => course !== undefined);
const lecturesIds = basket.map((course) => course?.lecture?.id).filter((course) => course !== undefined);
return [...classesIds, ...lecturesIds];
};
const addToBasket = (course: Course) => {
const selectBasketNames = () => basket.map(({ name }) => name);
const selectBasketCourses = () => {
const basketNames = selectBasketNames();
return basketNames.reduce((sum, basketName) => {
const course = courses.find(({ name }) => basketName === name);
return course === undefined ? sum : [...sum, course];
}, [] as Array<Course>);
};
const selectSchedulerEvents = () => {
return basket.reduce((res, el) => {
const { name } = el;
if (el.classes) {
const { time } = el.classes;
res.push({ ...el.classes, name, time: createClassTime(time) });
}
if (el.lecture) {
const { time } = el.lecture;
res.push({ ...el.lecture, name, time: createClassTime(time) });
}
return res;
}, [] as Array<SchedulerEvent>);
};
const selectBasketCourseGroups = (courseId: number) => {
const course = basket.find(({ id }) => id === courseId);
if (course !== undefined) {
return { lecture: course.lecture, classes: course.classes };
} else {
return { lecture: undefined, classes: undefined };
}
};
const changeHoveredGroup = (group: Group | null) => setHoveredGroup(group);
const addCourseToBasket = (course: Course) => {
const courseToBasket: Basket = {
name: course.name,
id: course.id,
classes: course.classes[0],
classes: course.classes !== undefined ? course.classes[0] : undefined,
lecture: course.lectures !== undefined ? course.lectures[0] : undefined,
};
setBasket([...basket, courseToBasket]);
@ -49,37 +98,25 @@ export const CoursesProvider = ({ children }: CoursesProviderProps) => {
const deleteFromBasket = (id: number) => setBasket(basket.filter((course) => course.id !== id));
const saveBasket = async () => {
const basketIds = selectBasketIds(basket);
const config = {
method: 'post' as const,
url: `${process.env.REACT_APP_API_URL}/api/v1/commisions/add?`,
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
data: JSON.stringify(basketIds),
};
const basketIds = selectBasketIds();
const action = (key: any) => (
<>
<button
<StyledCloseIcon
onClick={() => {
closeSnackbar(key);
}}
>
X
</button>
></StyledCloseIcon>
</>
);
try {
await axios.request(config);
await axiosInstance.post(`${process.env.REACT_APP_API_URL}/api/v1/commisions/user`, JSON.stringify(basketIds));
enqueueSnackbar('Plan został zapisany', {
variant: 'success',
action,
});
} catch (e) {
console.log('error: ', e);
enqueueSnackbar('Zapisywanie planu nie powiodło się', {
variant: 'error',
action,
@ -87,8 +124,8 @@ export const CoursesProvider = ({ children }: CoursesProviderProps) => {
}
};
const addGroup = (choosenGroup: Group, id: number) => {
const basketCourse = basket.filter((course) => course.id === id)[0];
const changeGroupInBasket = (choosenGroup: Group, courseId: number) => {
const basketCourse = basket.filter((course) => course.id === courseId)[0];
const { type } = choosenGroup;
if (type === GroupType.CLASS) {
setBasket(
@ -99,48 +136,72 @@ export const CoursesProvider = ({ children }: CoursesProviderProps) => {
basket.map((basket) => (basket.id === basketCourse.id ? { ...basket, lecture: choosenGroup } : basket)),
);
}
changeHoveredGroup(choosenGroup);
};
const restoreGroupInBasket = (restoreGroup: Group, courseId: number) => {
const basketCourse = basket.filter((course) => course.id === courseId)[0];
const { type } = restoreGroup;
if (type === GroupType.CLASS) {
setBasket(
basket.map((basket) => (basket.id === basketCourse.id ? { ...basket, classes: restoreGroup } : basket)),
);
} else if (type === GroupType.LECTURE) {
setBasket(
basket.map((basket) => (basket.id === basketCourse.id ? { ...basket, lecture: restoreGroup } : basket)),
);
}
};
const getNewestTimetable = async () => {
const config = {
method: 'get' as const,
url: `${process.env.REACT_APP_API_URL}/api/v1/assignments/getCurrentAssignments`,
headers: {
Authorization: `Bearer ${token}`,
},
};
try {
let { data: basket } = await axios.request(config);
if (basket === '') {
basket = [];
}
const { data } = await axiosInstance.get(
`${process.env.REACT_APP_API_URL}/api/v1/assignments/user`,
);
const basket = data === '' ? [] : data;
setBasket(basket);
} catch (e) {
console.log(e);
}
};
const fetchClasses = async () => {
const fetchCourses = async () => {
try {
const { data: courses } = await axios.get<Array<Course>>(
`${process.env.REACT_APP_API_URL}/api/v1/courses/getCoursesWithGroups`,
const { data: courses } = await axiosInstance.get<Array<Course>>(
`${process.env.REACT_APP_API_URL}/api/v1/courses/all?groups=true`,
);
courses.sort((a, b) => (a.name > b.name ? 1 : -1));
setCourses(courses);
const sortedCourses = courses.sort((a, b) => (a.name > b.name ? 1 : -1));
setCourses(sortedCourses);
} catch (e) {
console.log(e);
}
};
useEffect(() => {
fetchClasses();
if (token) {
setTimeout(() => {
fetchCourses();
getNewestTimetable();
}
}, [token]);
}, 200);
}, []);
return (
<coursesContext.Provider value={{ courses, basket, addToBasket, addGroup, deleteFromBasket, saveBasket }}>
<coursesContext.Provider
value={{
courses,
basket,
hoveredGroup,
addCourseToBasket,
changeHoveredGroup,
changeGroupInBasket,
deleteFromBasket,
restoreGroupInBasket,
saveBasket,
selectSchedulerEvents,
selectBasketNames,
selectBasketCourses,
selectBasketCourseGroups,
}}
>
{children}
</coursesContext.Provider>
);

View File

@ -1,29 +0,0 @@
// import { Group, Course } from '../types';
export enum Types {
addToBasket = 'ADD_CHOOSEN_COURSE',
removeChoosenCourse = 'REMOVE_CHOOSEN_COURSE',
addGroup = 'ADD_CHOOSEN_GROUP',
removeChoosenGroup = 'REMOVE_CHOOSEN_GROUP',
}
// type ChoosenCoursesPayload = {
// [Types.addToBasket]: {};
// };
// type ChoosenGroupsPayload = {
// [Types.Create]: {
// id: number;
// name: string;
// price: number;
// };
// };
// export const choosenGroupsReducer = (state, action) => {
// switch (action.type) {
// case Types.addGroup:
// return add;
// }
// };
//https://dev.to/elisealcala/react-context-with-usereducer-and-typescript-4obm

View File

@ -7,7 +7,7 @@ export interface Basket {
id: number;
name: string;
lecture?: Group;
classes: Group;
classes?: Group;
}
export interface Group {
@ -24,11 +24,21 @@ export interface Course {
id: number;
name: string;
lectures?: Array<Group>;
classes: Array<Group>;
classes?: Array<Group>;
}
export interface User {
name?: string;
surname?: string;
token: string | null;
}
export interface SchedulerEvent {
id: number;
day: number;
time: [string, string];
lecturer: string;
room: string;
type: GroupType;
capacity?: number;
name: string;
}

View File

@ -0,0 +1,15 @@
import axios from 'axios';
export const axiosInstance = axios.create();
axiosInstance.interceptors.request.use(
(config) => {
const token = sessionStorage.getItem('userToken');
config.headers['Authorization'] = 'Bearer ' + token;
config.headers['Content-Type'] = 'application/json';
return config;
},
(error) => {
Promise.reject(error);
},
);

80
src/utils/index.ts Normal file
View File

@ -0,0 +1,80 @@
import { courseStartTimeToEventRow } from '../constants/index';
import { SchedulerEvent } from '../types';
export const createClassTime = (startTime: string): [string, string] => {
const startTimeMapped = courseStartTimeToEventRow[startTime];
const endTime = Object.keys(courseStartTimeToEventRow).find(
(key) => courseStartTimeToEventRow[key] === startTimeMapped + 1,
)!;
return [startTime, endTime];
};
export const selectGroupsToShow = (schedulerEvents: Array<SchedulerEvent>, index: number) => {
return schedulerEvents.filter(({ time }: { time: [string, string] }) => courseStartTimeToEventRow[time[0]] === index);
};
type Procedure = (...args: any[]) => any;
interface Debounce {
(...args: any[]): any;
clear: () => void;
flush: () => void;
}
export const debounce = (func: Procedure, wait: number, immediate: boolean = false) => {
let timeout: number | null;
let args: any;
let context: any;
let result: any;
const later = () => {
timeout = null;
if (!immediate) {
result = func.apply(context, args);
context = args = null;
}
};
const debouncedFunc: Procedure = function (this: any) {
context = this;
args = arguments;
const callNow = immediate && !timeout;
if (!timeout) {
timeout = window.setTimeout(later, wait);
}
if (callNow) {
result = func.apply(context, args);
context = args = null;
}
return result;
};
const clear = () => {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
};
const flush = () => {
if (timeout) {
result = func.apply(context, args);
context = args = null;
clearTimeout(timeout);
timeout = null;
}
};
const debounced: Debounce = (() => {
const f: any = debouncedFunc;
f.clear = clear;
f.flush = flush;
return f;
})();
return debounced;
};

11686
yarn.lock Normal file

File diff suppressed because it is too large Load Diff