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 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, "private": true,
"dependencies": { "dependencies": {
"@material-ui/core": "^4.10.0", "@material-ui/core": "^4.10.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "^4.0.0-alpha.56", "@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", "axios": "^0.19.2",
"notistack": "^1.0.1", "notistack": "^1.0.1",
"react": "^16.13.1", "react": "16.8.0",
"react-click-away-listener": "^1.4.3", "react-click-away-listener": "^1.4.3",
"react-dom": "^16.13.1", "react-dom": "^16.13.1",
"react-scripts": "3.4.1", "react-scripts": "3.4.1",
"styled-components": "^5.1.1" "styled-components": "^5.1.1"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^24.9.1",
"@types/lodash": "^4.14.162",
"@types/node": "^12.12.54",
"@types/react": "^16.9.46", "@types/react": "^16.9.46",
"@types/react-dom": "^16.9.8", "@types/react-dom": "^16.9.8",
"@types/styled-components": "^5.1.2", "@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" "typescript": "^3.9.7"
}, },
"optionalDependencies": {},
"scripts": { "scripts": {
"start": "react-scripts start", "start": "react-scripts start",
"build": "react-scripts build", "build": "react-scripts build",
"test": "react-scripts test", "test": "react-scripts test",
"eject": "react-scripts eject", "eject": "react-scripts eject"
"lint": "eslint src/*.{js,ts,tsx} --quiet --fix" },
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
}, },
"browserslist": { "browserslist": {
"production": [ "production": [

View File

@ -1,24 +1,42 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="pl"> <html lang="en">
<head>
<head>
<meta charset="utf-8" /> <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="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta name="description" content="Web site created using create-react-app" /> <meta
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo.svg" /> 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" /> Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
<link href="https://fonts.googleapis.com/css2?family=Roboto+Condensed:wght@400;700&display=swap" rel="stylesheet"> work correctly both with client-side routing and a non-root public URL.
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap" rel="stylesheet"> Learn how to configure a non-root public URL by running `npm run build`.
<title>PlanNaPlan</title> -->
</head> <title>React App</title>
</head>
<body> <body>
<noscript>Potrzebujesz włączyć JavaScript, żeby otworzyć tę aplikację</br>You need to enable JavaScript to run this <noscript>You need to enable JavaScript to run this app.</noscript>
app.</noscript>
<div id="root"></div> <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.
</html> 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 Topbar from './Topbar';
import { Transfer } from './Transfer'; import { Transfer } from './Transfer';
import { Admin } from './Admin';
import { Scheduler } from './Scheduler'; import { Scheduler } from './Scheduler';
import { Rightbar } from './Rightbar'; import { Rightbar } from './Rightbar';
import styled from 'styled-components'; import styled from 'styled-components';
@ -8,8 +9,10 @@ import styled from 'styled-components';
const Wrapper = styled.div` const Wrapper = styled.div`
display: flex; display: flex;
height: calc(100vh - 80px); height: calc(100vh - 80px);
background-color: #ECEEF4; background-color: #eceef4;
padding: 20px; padding-top: 20px;
padding-bottom: 20px;
padding-right: 20px;
`; `;
export const App = () => { export const App = () => {
@ -24,6 +27,7 @@ export const App = () => {
<Topbar handleTransfer={handleTransfer} /> <Topbar handleTransfer={handleTransfer} />
<Transfer isOpen={isOpenTransfer} handleClose={handleTransfer} /> <Transfer isOpen={isOpenTransfer} handleClose={handleTransfer} />
<Wrapper> <Wrapper>
{/* <Admin/> */}
<Scheduler /> <Scheduler />
<Rightbar /> <Rightbar />
</Wrapper> </Wrapper>

View File

@ -1,40 +1,40 @@
import React, { useState, useContext, MouseEvent } from 'react'; import React, { useState, useContext } from 'react';
import Collapse from '@material-ui/core/Collapse'; import Collapse from '@material-ui/core/Collapse';
import { ReactComponent as Expand } from '../assets/expand.svg'; 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 { coursesContext } from '../contexts/CoursesProvider';
import styled from 'styled-components'; import styled, { css } from 'styled-components';
import { makeStyles } from '@material-ui/core/styles'; 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` const CourseCardWrapper = styled.div`
position: relative; position: relative;
display: flex; display: flex;
min-height: 40px; min-height: 40px;
background-color: rgb(100, 181, 246); background-color: #b5d2e0;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
flex-direction: column; flex-direction: column;
margin-top: 10px; margin-top: 10px;
border-radius: 10px; border-radius: 10px;
cursor: pointer; cursor: pointer;
align-items: stretch; align-items: stretch;
box-shadow: 9px 9px 8px -2px rgba(0, 0, 0, 0.59); box-shadow: 9px 9px 8px -2px rgba(0, 0, 0, 0.59);
`; `;
const TitleWrapper = styled.div` const TitleWrapper = styled.div`
font-size: 14px;
font-weight: 550;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 10px; padding: 10px 10px 10px 2px;
` `;
const BinIcon = styled(Bin)` const BinIcon = styled(DeleteIcon)`
width: 20px; max-width: 30px;
height: 20px; min-width: 30px;
max-width: 20px;
min-width: 20px;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
fill: white; fill: white;
@ -51,37 +51,63 @@ const CourseName = styled.div`
const ClassGroupStyled = styled.div` const ClassGroupStyled = styled.div`
position: relative; position: relative;
padding-top: 1px; padding-top: 1px;
padding-bottom: 1px; padding-bottom: 5px;
:hover { :hover {
cursor: pointer; cursor: pointer;
background-color: #9ed3ff; background-color: #9ed3ff;
} }
:last-child {
border-radius: 0 0 10px 10px;
}
`; `;
interface ExpandIconProps { interface ExpandIconProps {
isSelected: boolean; selected: boolean;
} }
const ExpandIcon = styled(Expand) <ExpandIconProps>` export const ExpandIcon = styled(Expand)<ExpandIconProps>`
width: 20px; width: 20px;
height: 20px; height: 20px;
max-width: 20px; max-width: 20px;
min-width: 20px; min-width: 20px;
transition: 0.2s; 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; font-size: 12px;
position: absolute; position: absolute;
border-radius: 15px; border-radius: 15px;
background-color: #00506b; background-color: ${({ groupType }) => (groupType === 'CLASS' ? '#FFDC61' : '#9ed3ff')};
border: 2px solid; border: 2px solid white;
min-width: 45px; min-width: 45px;
top: 5px; top: 5px;
left: 5px; left: 5px;
color: white; color: black;
font-weight: bold; `;
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({ const useStyles = makeStyles({
@ -89,51 +115,100 @@ const useStyles = makeStyles({
maxHeight: '244px', maxHeight: '244px',
overflowY: 'auto', overflowY: 'auto',
'&::-webkit-scrollbar': { '&::-webkit-scrollbar': {
width: '0.4em', width: '0.3em',
borderStyle: 'none',
}, },
'&::-webkit-scrollbar-track': { '&::-webkit-scrollbar-track': {
'-webkit-box-shadow': 'inset 0 0 6px rgba(1,0,0,0.1)', borderRadius: '10px',
}, },
'&::-webkit-scrollbar-thumb': { '&::-webkit-scrollbar-thumb': {
borderRadius: '10px', borderRadius: '10px',
backgroundColor: '#d4b851', backgroundColor: '#4b4b4b',
outline: '1px solid slategrey',
}, },
}, },
}); });
interface CourseCardProps { interface CourseCardProps {
course: Course; course: Course;
} }
export const CourseCard = ({ course }: CourseCardProps) => { export const CourseCard = ({ course }: CourseCardProps) => {
const classes = useStyles(); const classes = useStyles();
const { addGroup, deleteFromBasket } = useContext(coursesContext)!; const {
hoveredGroup,
changeGroupInBasket,
deleteFromBasket,
selectBasketCourseGroups,
changeHoveredGroup,
} = useContext(coursesContext)!;
const [isSelected, setSelected] = useState(false); const [isSelected, setSelected] = useState(false);
const groups = course.lectures === undefined ? course.classes : [...course.lectures, ...course.classes]; const groups = [...course.lectures!, ...course.classes!];
const basketCourseGroups = useMemo(() => selectBasketCourseGroups(course.id), []);
const onGroupClick = (group: Group, id: number) => addGroup(group, 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 ( return (
<CourseCardWrapper> <CourseCardWrapper>
<TitleWrapper> <TitleWrapper onClick={() => setSelected(!isSelected)}>
<BinIcon onClick={() => deleteFromBasket(course.id)}></BinIcon> <BinIcon
onClick={(e) => {
e.stopPropagation();
deleteFromBasket(course.id);
setSelected(false);
}}
></BinIcon>
<CourseName onClick={() => setSelected(!isSelected)}>{course.name}</CourseName> <CourseName onClick={() => setSelected(!isSelected)}>{course.name}</CourseName>
<ExpandIcon onClick={() => setSelected(!isSelected)} isSelected={isSelected} /> <ExpandIcon onClick={() => setSelected(!isSelected)} selected={isSelected} />
</TitleWrapper> </TitleWrapper>
<Collapse className={classes.expanded} in={isSelected} timeout="auto" unmountOnExit> <Collapse className={classes.expanded} in={isSelected} timeout="auto" unmountOnExit>
{groups {groups.map((group: Group, index) => (
.sort((a, b) => b.type.localeCompare(a.type)) <ClassGroupStyled
.map((group, index) => ( key={index}
<ClassGroupStyled key={index} onClick={() => onGroupClick(group, course.id)}> onClick={() => onGroupClick(group, course.id)}
<TypeClass>{group.type === 'CLASS' ? 'Ćw.' : 'Wyk.'}</TypeClass> onMouseEnter={() => {
<p> if (group.type === GroupType.CLASS) {
{group.time} {group.room} <br></br> {group.lecturer} changeGroupInBasket(group, course.id);
</p> // setTimeout(()=> { changeHoveredGroup(courseClasses)},[500])
</ClassGroupStyled> }
))} 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> </Collapse>
</CourseCardWrapper> </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 { coursesContext } from '../contexts/CoursesProvider';
import { Course } from '../types'; import { Course } from '../types';
import styled from 'styled-components'; import styled from 'styled-components';
const DropdownContainer = styled.div` const DropdownContainer = styled.div`
position: relative; position: relative;
z-index: 99999999; z-index: 99999999;
max-height: 420px; max-height: 396px;
border-radius: 3px; border-radius: 3px;
overflow-y: auto; overflow-y: auto;
opacity: 0.97;
box-shadow: 0.05em 0.2em 0.6em rgba(0, 0, 0, 0.2); box-shadow: 0.05em 0.2em 0.6em rgba(0, 0, 0, 0.2);
scroll-snap-type: y mandatory; scroll-snap-type: y mandatory;
scroll-behavior: smooth; scroll-behavior: smooth;
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
background-color: #f2f4f7;
border-radius: 10px; border-radius: 10px;
background-color: #f5f5f5;
} }
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 12px; background-color: #f2f4f7;
background-color: #f5f5f5; width: 5px;
border-style: none;
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
border-radius: 10px; border-radius: 10px;
background-color: black; background-color: #4b4b4b;
border: 1px solid;
} }
`; `;
@ -33,7 +32,7 @@ const CourseContainer = styled.div`
padding: 5px; padding: 5px;
padding-left: 20px; padding-left: 20px;
background-color: #f2f4f7; background-color: #f2f4f7;
font-size: 18px; font-size: 16px;
font-weight: 500; font-weight: 500;
scroll-snap-align: end; scroll-snap-align: end;
:hover { :hover {
@ -48,27 +47,22 @@ interface DropdownProps {
handleCloseDropdown: () => void; handleCloseDropdown: () => void;
} }
export const Dropdown = forwardRef(({ open, input, handleCloseDropdown }: DropdownProps, ref: any) => { export const Dropdown = ({ open, input, handleCloseDropdown }: DropdownProps) => {
//courses - choosenCourses const { courses, selectBasketNames, addCourseToBasket } = useContext(coursesContext)!;
const basketNames = useMemo(() => selectBasketNames(), [selectBasketNames]);
const [filteredCourses, setFilteredCourses] = useState<Array<Course>>([]); const [filteredCourses, setFilteredCourses] = useState<Array<Course>>([]);
const { courses, basket, addToBasket } = useContext(coursesContext)!; const onCourseClick = (event: MouseEvent) => {
const target = event.currentTarget;
useEffect(() => { if (target.id && target.textContent) {
console.log('wut'); const course = filteredCourses.find(({ id }) => id.toString() === target.id)!;
}, [open, input, handleCloseDropdown]); addCourseToBasket(course);
handleCloseDropdown();
useEffect(() => { }
console.log('input is: ', input); };
}, [input]);
useEffect(() => {
console.log('is open: ', open);
}, [open]);
useEffect(() => { useEffect(() => {
const filterCourses = (input: string) => { const filterCourses = (input: string) => {
const choosenCoursesNames = basket.map(({ name }) => name.trim());
const filteredCourses = courses.filter( const filteredCourses = courses.filter(
({ name }) => ({ name }) =>
name name
@ -80,24 +74,12 @@ export const Dropdown = forwardRef(({ open, input, handleCloseDropdown }: Dropdo
.toLowerCase() .toLowerCase()
.normalize('NFD') .normalize('NFD')
.replace(/[\u0300-\u036f]/g, ''), .replace(/[\u0300-\u036f]/g, ''),
) && !choosenCoursesNames.includes(name), ) && !basketNames.includes(name),
); );
setFilteredCourses(filteredCourses); setFilteredCourses(filteredCourses);
}; };
console.log("filtering courses");
filterCourses(input); filterCourses(input);
}, [open, input, basket]); }, [basketNames, courses, input]);
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();
}
};
return ( return (
<DropdownContainer> <DropdownContainer>
@ -112,4 +94,4 @@ export const Dropdown = forwardRef(({ open, input, handleCloseDropdown }: Dropdo
)} )}
</DropdownContainer> </DropdownContainer>
); );
}); };

View File

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

View File

@ -2,28 +2,24 @@ import React, { useContext } from 'react';
import { CourseCard } from './CourseCard'; import { CourseCard } from './CourseCard';
import { coursesContext } from '../contexts/CoursesProvider'; import { coursesContext } from '../contexts/CoursesProvider';
import styled from 'styled-components'; import styled from 'styled-components';
import { debounce } from 'lodash'; import { debounce } from '../utils/index';
const RightbarStyled = styled.div` const RightbarWrapper = styled.div`
padding-top: 10px; padding: 15px;
padding-left: 15px;
padding-right: 15px;
text-align: center; text-align: center;
height: 100%; height: 100%;
width: 300px; width: 350px;
overflow-y: scroll; overflow-y: scroll;
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
border-radius: 10px; border-radius: 10px;
background-color: #f5f5f5;
} }
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 12px; width: 5px;
background-color: #f5f5f5; border-style: none;
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
border-radius: 10px; border-radius: 10px;
background-color: black; background-color: #4b4b4b;
border: 1px solid;
} }
background-color: white; background-color: white;
border-radius: 5px; border-radius: 5px;
@ -34,36 +30,34 @@ const SaveButton = styled.div`
justify-content: center; justify-content: center;
align-items: center; align-items: center;
user-select: none; user-select: none;
background-color: #417cab; background-color: #43a047;
border-radius: 10px; border-radius: 10px;
cursor: pointer; cursor: pointer;
height: 40px; height: 40px;
margin-bottom: 10px; margin-bottom: 10px;
&:hover { &: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 = () => { export const Rightbar = () => {
const { courses, basket, saveBasket } = useContext(coursesContext)!; const { selectBasketCourses, saveBasket } = useContext(coursesContext)!;
const getBasketGroups = () => {
const names = basket.map(({ name }) => name);
return courses.filter(({ name }) => names.includes(name));
};
const filteredCourses = getBasketGroups();
const basketCourses = selectBasketCourses();
const handleSave = debounce(() => saveBasket(), 500); const handleSave = debounce(() => saveBasket(), 500);
//need to insert student name from db and course maybe based on current time or from db too
return ( return (
<RightbarStyled> <RightbarWrapper>
<SaveButton onClick={handleSave}>ZAPISZ</SaveButton> <SaveButton onClick={handleSave}>ZAPISZ</SaveButton>
{filteredCourses.map((course, index) => ( {basketCourses.map((course) => (
<CourseCard course={course} key={index} /> <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 { useState } from 'react';
import { SchedulerEvents } from './SchedulerEvents'; import { SchedulerEvents } from './SchedulerEvents';
import { days, hours } from '../constants/index'; import { days, hours } from '../constants/index';
@ -11,6 +11,7 @@ const SchedulerWrapper = styled.div`
padding: 10px 40px 25px 10px; padding: 10px 40px 25px 10px;
border-radius: 5px; border-radius: 5px;
margin-right: 20px; margin-right: 20px;
margin-left: 20px;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@ -42,9 +43,9 @@ interface TableCellProps {
} }
const TableCell = styled.div<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-style: ${({ isHourColumn }) => !isHourColumn && 'none solid dotted none'};
border-color: rgb(242, 243, 245); border-color: rgb(235, 235, 235);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: ${({ isHourColumn }) => (isHourColumn ? 'flex-end' : 'center')}; justify-content: ${({ isHourColumn }) => (isHourColumn ? 'flex-end' : 'center')};
@ -55,7 +56,7 @@ const TableCell = styled.div<TableCellProps>`
user-select: none; user-select: none;
border-collapse: collapse; border-collapse: collapse;
:nth-child(2) { :nth-child(2) {
border-left: 2px solid rgb(242, 243, 245); border-left: 1px solid rgb(235, 235, 235);
} }
font-weight: bold; font-weight: bold;
`; `;
@ -63,18 +64,13 @@ const TableCell = styled.div<TableCellProps>`
export const Scheduler = () => { export const Scheduler = () => {
const cellRef = useRef<HTMLDivElement>(null); const cellRef = useRef<HTMLDivElement>(null);
const [cellWidth, setCellWidth] = useState(0); const [cellWidth, setCellWidth] = useState(0);
const [cellTop, setCellTop] = useState(0);
const [cellHeight, setCellHeight] = useState(0); const [cellHeight, setCellHeight] = useState(0);
console.log('cell height: ', cellHeight);
useEffect(() => { useEffect(() => {
const handleResize = () => { const handleResize = () => {
if (cellRef.current) { if (cellRef.current) {
setCellWidth(cellRef.current.getBoundingClientRect().width); setCellWidth(cellRef.current.getBoundingClientRect().width);
setCellTop(cellRef.current.getBoundingClientRect().top);
setCellHeight(cellRef.current.getBoundingClientRect().height); setCellHeight(cellRef.current.getBoundingClientRect().height);
cellRef.current.style.backgroundColor = 'blue';
} }
}; };
handleResize(); handleResize();
@ -83,52 +79,52 @@ export const Scheduler = () => {
}, []); }, []);
return ( return (
<> <SchedulerWrapper>
<SchedulerWrapper> <TableHead>
<TableHead> {days.map((day, indexCell) =>
{days.map((day, indexCell) => indexCell === 0 ? (
indexCell === 0 ? ( <TableCell isHourColumn={true} key={indexCell}>
<TableCell isHourColumn={true} key={indexCell}> {day}
{day} </TableCell>
</TableCell> ) : (
) : ( <TableCell style={{ borderStyle: 'none none solid none' }} key={indexCell}>
<TableCell style={{ borderStyle: 'none none solid none' }} key={indexCell}> {day}
{day} </TableCell>
</TableCell> ),
), )}
)} </TableHead>
</TableHead> <TableBody>
<TableBody> {hours.map((hour, indexRow) => (
{hours.map((hour, indexRow) => ( <TableRow key={indexRow}>
<TableRow key={indexRow}> {[hour, '', '', '', '', ''].map((value, indexCell) =>
{[hour, '', '', '', '', ''].map((value, indexCell) => indexCell === 0 ? (
indexCell === 0 ? ( <TableCell isHourColumn={true} cellHeight={cellHeight} key={`${indexRow}${indexCell}`}>
<TableCell isHourColumn={true} cellHeight={cellHeight} key={`${indexRow}${indexCell}`}> {value}
{value} </TableCell>
</TableCell> ) : indexRow === 0 && indexCell === 1 ? (
) : indexRow === 0 && indexCell === 1 ? ( <TableCell ref={cellRef} key={`${indexRow}${indexCell}`}>
<TableCell ref={cellRef} key={`${indexRow}${indexCell}`}> {value}
{value} </TableCell>
</TableCell> ) : indexRow === 23 ? (
) : indexRow === 23 ? ( <TableCell style={{ borderBottom: '1px solid rgb(235, 235, 235)' }} key={`${indexRow}${indexCell}`}>
<TableCell style={{ borderBottom: '2px solid rgb(242, 243, 245)' }} key={`${indexRow}${indexCell}`}> {value}
{value} </TableCell>
</TableCell> ) : indexRow === 5 ? (
) : indexCell === 5 ? ( <TableCell style={{ borderBottom: '1px solid rgb(235, 235, 235)' }} key={`${indexRow}${indexCell}`}>
<TableCell key={`${indexRow}${indexCell}`}>{value}</TableCell> {value}
) : indexRow % 2 !== 0 ? ( </TableCell>
<TableCell style={{ borderBottom: '2px solid rgb(242, 243, 245)' }} key={`${indexRow}${indexCell}`}> ) : indexRow % 2 !== 0 ? (
{value} <TableCell style={{ borderBottom: '1px solid rgb(235, 235, 235)' }} key={`${indexRow}${indexCell}`}>
</TableCell> {value}
) : ( </TableCell>
<TableCell key={`${indexRow}${indexCell}`}>{value}</TableCell> ) : (
), <TableCell key={`${indexRow}${indexCell}`}>{value}</TableCell>
)} ),
</TableRow> )}
))} </TableRow>
<SchedulerEvents cellTop={cellTop} cellWidth={cellWidth} cellHeight={cellHeight} /> ))}
</TableBody> <SchedulerEvents cellWidth={cellWidth} cellHeight={cellHeight} />
</SchedulerWrapper> </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 { SchedulerRow } from './SchedulerRow';
import { coursesContext } from '../contexts/CoursesProvider'; import { coursesContext } from '../contexts/CoursesProvider';
import { Group, Basket } from '../types'; import { selectGroupsToShow } from '../utils/index';
import { ROWS_COUNT } from '../constants';
interface SchedulerEventsProps { interface SchedulerEventsProps {
cellTop: number;
cellWidth: number; cellWidth: number;
cellHeight: number; cellHeight: number;
} }
export const SchedulerEvents = ({ cellTop, cellWidth, cellHeight }: SchedulerEventsProps) => { export const SchedulerEvents = ({ cellWidth, cellHeight }: SchedulerEventsProps) => {
const { basket } = useContext(coursesContext)!; const { selectSchedulerEvents } = useContext(coursesContext)!;
console.log(`values: cellTop: ${cellTop}, cellWidth: ${cellWidth}, cellHeight: ${cellHeight}`);
const [choosenGroupsMappedToEvents, setChoosenGroupsMappedToEvents] = useState<any>([]);
const groupTimeToEventRowMapping: { [time: string]: number } = { const schedulerEvents = selectSchedulerEvents();
'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]);
return ( return (
<div> <div>
{[...Array(6)].map((_, index) => ( {[...Array(ROWS_COUNT)].map((_, index) => (
<SchedulerRow <SchedulerRow
key={index} key={index}
groups={choosenGroupsMappedToEvents.filter((group: any) => group.eventRow === index)} groups={selectGroupsToShow(schedulerEvents, index)}
indexRow={index} indexRow={index}
cellTop={ rowTop={
index === 0 index === 0
? cellHeight / 2 ? cellHeight / 2
: index === 1 : index === 1

View File

@ -1,92 +1,163 @@
import React, { MouseEvent, useState } from 'react'; import React, { Fragment, MouseEvent, useState, useEffect, useContext } from 'react';
import { Group, GroupType } from '../types'; import { GroupType, SchedulerEvent } from '../types';
import styled from 'styled-components/macro'; import styled, { css } from 'styled-components/macro';
import Popover from '@material-ui/core/Popover'; import Popover from '@material-ui/core/Popover';
import Typography from '@material-ui/core/Typography';
import { makeStyles, createStyles, Theme } from '@material-ui/core/styles'; 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) => const useStyles = makeStyles((theme: Theme) =>
createStyles({ createStyles({
popover: { popover: {
pointerEvents: 'none', fontSize: '14px',
}, },
paper: { paper: {
padding: theme.spacing(1), padding: '15px 15px 15px 15px',
marginLeft: 5, textAlign: 'left',
textAlign: 'center', lineHeight: `1 !important`,
}, },
}), }),
); );
interface ClassesWrapperProps { const PopoverSpan = styled.span`
font-weight: bold;
margin-right: 2px;
`;
interface SchedulerEventsWrapperProps {
eventIndex: number; eventIndex: number;
cellTop: number; rowTop: number;
cellWidth: number; cellWidth: number;
cellHeight: number; cellHeight: number;
} }
const ClassesWrapper = styled.div<ClassesWrapperProps>` const SchedulerEventsWrapper = styled.div<SchedulerEventsWrapperProps>`
position: absolute; position: absolute;
display: flex; display: flex;
top: ${({ cellTop }) => cellTop}px; top: ${({ rowTop }) => rowTop}px;
left: ${({ cellWidth, eventIndex }) => (cellWidth * 1) / 5 + 15 + cellWidth * eventIndex}px; left: ${({ cellWidth, eventIndex }) => (cellWidth * 1) / 5 + 4 + cellWidth * eventIndex}px;
width: ${({ cellWidth }) => cellWidth - 10}px; width: ${({ cellWidth }) => cellWidth - 10}px;
height: ${({ cellHeight }) => cellHeight * 3}px; height: ${({ cellHeight }) => cellHeight * 3}px;
z-index: 2; z-index: 2;
padding-left: 10px; padding-left: 10px;
`; `;
interface ClassesProps { interface SchedulerEventProps {
cellWidth: number;
cellHeight: number; cellHeight: number;
groupType: GroupType; groupType: GroupType;
isHovered: boolean;
} }
const Classes = styled.div<ClassesProps>` const StyledSchedulerEvent = styled.div<SchedulerEventProps>`
display: flex; display: flex;
flex: 1; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
z-index: 2; z-index: 20000;
font-size: 0.65vw;
line-height: normal;
border-radius: 10px; border-radius: 10px;
height: ${({ cellHeight }) => cellHeight * 3}px; height: ${({ cellHeight }) => cellHeight * 3}px;
width: ${({ cellWidth }) => (cellWidth * 3) / 4}px;
margin-right: 5px; margin-right: 5px;
text-align: left; padding: 5px 5px 0 5px;
background-color: ${({ groupType }) => (groupType === 'CLASS' ? '#FFDC61' : '#9ed3ff')}; text-align: center;
box-shadow: 9px 9px 8px -2px rgba(0, 0, 0, 0.59); 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 { interface SchedulerRowProps {
groups: Array<Group & { name: string }>; groups: Array<SchedulerEvent>;
indexRow: number; indexRow: number;
cellTop: number; rowTop: number;
cellWidth: number; cellWidth: number;
cellHeight: 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 classes = useStyles();
const groupsPerDay = getGroupsPerDay(groups);
const [anchorEl, setAnchorEl] = React.useState<HTMLDivElement | null>(null); const [anchorEl, setAnchorEl] = React.useState<HTMLDivElement | null>(null);
const [popoverId, setPopoverId] = useState<string | null>(null); const [popoverId, setPopoverId] = useState<string | null>(null);
//looks weird //looks weird
const handlePopoverOpen = (event: MouseEvent<HTMLDivElement, globalThis.MouseEvent>) => { const handlePopoverOpen = (event: MouseEvent<HTMLDivElement, globalThis.MouseEvent>) => {
console.log('I was clicked!!!!');
setAnchorEl(event.currentTarget); setAnchorEl(event.currentTarget);
setPopoverId(event.currentTarget.id); setPopoverId(event.currentTarget.id);
}; };
const handlePopoverClose = () => { const handlePopoverClose = (e: MouseEvent<any>) => {
setAnchorEl(null); console.log('current target:', e.currentTarget);
console.log(' target:', e.target);
setPopoverId(null); setPopoverId(null);
setAnchorEl(null);
console.log('click awayyy');
}; };
useEffect(() => {
console.log('anchorEl: ', anchorEl);
}, [anchorEl]);
const open = Boolean(anchorEl); const open = Boolean(anchorEl);
const id = open ? 'simple-popover' : undefined;
return ( return (
<div> <div>
{[...Array(5)].map((_, eventIndex) => ( {[...Array(MONDAY_TO_FRIDAY)].map((_, eventIndex) => (
<ClassesWrapper <SchedulerEventsWrapper
eventIndex={eventIndex} eventIndex={eventIndex}
cellTop={cellTop} rowTop={rowTop}
cellWidth={cellWidth} cellWidth={cellWidth}
cellHeight={cellHeight} cellHeight={cellHeight}
key={eventIndex} key={eventIndex}
@ -95,27 +166,35 @@ export const SchedulerRow = ({ groups, indexRow, cellTop, cellWidth, cellHeight
{groups.map( {groups.map(
(group, index) => (group, index) =>
group.day === eventIndex && ( group.day === eventIndex && (
<> <Fragment key={index}>
<Classes <StyledSchedulerEvent
onClick={() => { aria-describedby={id}
console.log('group: ', group); isHovered={group.id === hoveredGroup?.id}
}}
groupType={group.type} groupType={group.type}
cellWidth={cellWidth}
cellHeight={cellHeight} cellHeight={cellHeight}
id={`eventRow${indexRow}eventCol${eventIndex}${index}`} id={`eventRow${indexRow}eventCol${eventIndex}${index}`}
key={index} key={index}
aria-owns={open ? `mouse-over-popover` : undefined} aria-owns={open ? `mouse-over-popover` : undefined}
aria-haspopup="true" aria-haspopup="true"
onMouseEnter={(e) => handlePopoverOpen(e)} onClick={(e) => handlePopoverOpen(e)}
onMouseLeave={handlePopoverClose}
> >
<div> <ClassWrap>
<p style={{ fontWeight: 700 }}>{groups[index].name}</p> <BoldParagraph isThree={groupsPerDay[group.day] >= 3}>{groups[index].name}</BoldParagraph>
<p>{groups[index].room}</p> {groupsPerDay[group.day] < 3 ? (
</div> <TextWrapper>
</Classes> <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 <Popover
id={`mouse-over-popover`} id={id}
className={classes.popover} className={classes.popover}
classes={{ classes={{
paper: classes.paper, paper: classes.paper,
@ -133,16 +212,34 @@ export const SchedulerRow = ({ groups, indexRow, cellTop, cellWidth, cellHeight
onClose={handlePopoverClose} onClose={handlePopoverClose}
disableRestoreFocus disableRestoreFocus
> >
<Typography> <div
<p>{groups[index].name}</p> style={{ display: 'flex', flexDirection: 'column', zIndex: 20000 }}
<p>{groups[index].lecturer}</p> onClick={() => {
<p>{groups[index].room}</p> console.log('XDD');
</Typography> }}
>
<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> </Popover>
</> </Fragment>
), ),
)} )}
</ClassesWrapper> </SchedulerEventsWrapper>
))} ))}
</div> </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 React, { useState, MouseEvent, ChangeEvent, useEffect, useCallback } from 'react';
import Transfer from '../assets/transfer.png';
import Search from '../assets/search.svg';
import { ReactComponent as Close } from '../assets/close.svg'; import { ReactComponent as Close } from '../assets/close.svg';
import ProfileIcon from '../assets/account.svg'; import ProfileIcon from '../assets/account.svg';
import { Profile } from './Profile'; import { Profile } from './Profile';
@ -9,9 +7,10 @@ import PolishIcon from '../assets/poland.svg';
import EnglishIcon from '../assets/united-kingdom.svg'; import EnglishIcon from '../assets/united-kingdom.svg';
import styled from 'styled-components/macro'; import styled from 'styled-components/macro';
import ClickAwayListener from 'react-click-away-listener'; import ClickAwayListener from 'react-click-away-listener';
import { SelectMenu } from './SelectMenu';
const Topbar = styled.div` const Topbar = styled.div`
background-color: #E3E5ED; background-color: #e3e5ed;
height: 80px; height: 80px;
padding: 5px; padding: 5px;
font-size: 24px; font-size: 24px;
@ -31,19 +30,22 @@ const LogoWrapper = styled.div`
const Logo = styled.img` const Logo = styled.img`
width: 70px; width: 70px;
height: 70px; height: 70px;
@media only screen and (max-width: 670px) { @media only screen and (max-width: 1533px) {
width: 60px; flex: auto;
height: 60px;
} }
`; `;
const Text = styled.div` const Text = styled.div`
margin-left: 10px; margin-left: 10px;
font-size: 1.4rem; font-size: 1.4rem;
user-select: none; user-select: none;
@media only screen and (max-width: 670px) { @media only screen and (max-width: 1533px) {
display: none; display: none;
} }
@media only screen and (max-width: 1828px) {
margin-right: 10px;
text-align: center;
}
`; `;
const FlexboxColumn = styled.div` const FlexboxColumn = styled.div`
@ -53,28 +55,37 @@ const FlexboxColumn = styled.div`
`; `;
const InputWrapper = styled.div` const InputWrapper = styled.div`
width: 100%;
display: flex; display: flex;
margin-top: 15px; margin-top: 15px;
max-height: 40px;
background-color: #f2f4f7; background-color: #f2f4f7;
border-radius: 6px; border-radius: 6px 6px 6px 6px;
align-items: center; padding-left: 6px;
&:hover {
background-color: #ffffff;
}
&:hover > input {
background-color: #ffffff;
}
`; `;
const Input = styled.input` const Input = styled.input`
font-family: 'Roboto', sans-serif;
font-size: 18px;
background-color: #f1f2f5; background-color: #f1f2f5;
font-size: 20px;
height: 40px; height: 40px;
max-height: 40px;
width: 100%; width: 100%;
border: none; border: none;
margin-left: 5px; margin-left: 5px;
border-top-left-radius: 6px;
border-bottom-left-radius: 6px;
&:focus { &:focus {
outline: none; outline: none;
} }
`; `;
const CloseIcon = styled(Close)` const CloseIcon = styled(Close)`
align-self: center;
width: 30px; width: 30px;
height: 30px; height: 30px;
margin-right: 5px; margin-right: 5px;
@ -103,9 +114,9 @@ const Icon = styled.img`
} }
`; `;
export const Flexbox = styled.div`
display: flex;
`;
interface TopbarProps { interface TopbarProps {
handleTransfer: (e: MouseEvent) => void; handleTransfer: (e: MouseEvent) => void;
@ -122,21 +133,22 @@ export default function ({ handleTransfer }: TopbarProps) {
const handleProfile = (event: MouseEvent<HTMLImageElement>) => setAnchorEl(event.currentTarget); 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 handleChange = (event: ChangeEvent<HTMLInputElement>) => setInput(event.target.value);
const handleClick = () => setOpen(true); const handleShowDropdown = () => setOpen(true);
const handleCloseDropdown = () => setOpen(false); const handleCloseDropdown = () => setOpen(false);
const handleClickAway = () => setOpen(false);
useEffect(() => { useEffect(() => {
clearInput && (setInput(''), handleClearInput()); if (clearInput) {
}, [clearInput]); setInput('');
handleClearInput();
}
}, [clearInput, handleClearInput]);
return ( return (
<Topbar> <Topbar>
@ -145,11 +157,26 @@ export default function ({ handleTransfer }: TopbarProps) {
<Text> plan na plan </Text> <Text> plan na plan </Text>
</LogoWrapper> </LogoWrapper>
<FlexboxColumn> <FlexboxColumn>
<ClickAwayListener onClickAway={handleClickAway}> <ClickAwayListener onClickAway={handleCloseDropdown}>
<InputWrapper> <Flexbox>
<Input placeholder="Wyszukaj przedmiot..." onChange={handleChange} onClick={handleClick} value={input} /> {/* <SelectMenu /> */}
<CloseIcon onClick={handleClearInput} />
</InputWrapper> <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} /> <Dropdown open={open} input={input} handleCloseDropdown={handleCloseDropdown} />
</ClickAwayListener> </ClickAwayListener>
</FlexboxColumn> </FlexboxColumn>
@ -159,7 +186,7 @@ export default function ({ handleTransfer }: TopbarProps) {
{/* <Icon alt="transfer" src={Transfer} onClick={handleTransfer} /> */} {/* <Icon alt="transfer" src={Transfer} onClick={handleTransfer} /> */}
<Icon alt="change_language" src={isPolish ? EnglishIcon : PolishIcon} onClick={onLangChange} /> <Icon alt="change_language" src={isPolish ? EnglishIcon : PolishIcon} onClick={onLangChange} />
<Icon alt="profile" src={ProfileIcon} onClick={handleProfile} /> <Icon alt="profile" src={ProfileIcon} onClick={handleProfile} />
<Profile anchorEl={anchorEl} handleClose={handleClose} /> <Profile anchorEl={anchorEl} handleClose={handleCloseProfile} />
</IconWrapper> </IconWrapper>
</Topbar> </Topbar>
); );

View File

@ -1,37 +1,44 @@
export const days = [ export const days = ['', 'Poniedziałek', 'Wtorek', 'Środa', 'Czwartek', 'Piątek'];
"",
"Poniedziałek",
"Wtorek",
"Środa",
"Czwartek",
"Piątek",
];
export const hours = [ export const hours = [
"8:00", '8:00',
"", '',
"9:00", '9:00',
"", '',
"10:00", '10:00',
"", '',
"11:00", '11:00',
"", '',
"12:00", '12:00',
"", '',
"13:00", '13:00',
"", '',
"14:00", '14:00',
"", '',
"15:00", '15:00',
"", '',
"16:00", '16:00',
"", '',
"17:00", '17:00',
"", '',
"18:00", '18:00',
"", '',
"19:00", '19:00',
"", '',
]; ];
export const MONDAY_TO_FRIDAY = 5;
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 React, { useState, useEffect, createContext, ReactNode } from 'react';
import { User } from '../types'; import { User } from '../types';
import axios from 'axios'; import { axiosInstance } from '../utils/axiosInstance';
export interface CASContext { export interface CASContext {
user?: User; user?: User;
logout: () => void; logout: () => void;
token?: string | null;
} }
export const CASContext = createContext<CASContext | undefined>(undefined); export const CASContext = createContext<CASContext | undefined>(undefined);
@ -15,29 +16,28 @@ export interface CASProviderProps {
export const CASProvider = ({ children }: CASProviderProps) => { export const CASProvider = ({ children }: CASProviderProps) => {
const [user, setUser] = useState<User | undefined>(undefined); const [user, setUser] = useState<User | undefined>(undefined);
const [token, setToken] = useState<string | null>(null);
useEffect(() => { 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(); 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() { function logout() {
redirectToCASLogoutService(); 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`); 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 React, { useState, createContext, useEffect, ReactNode } from 'react';
import { Course, Group, Basket, GroupType } from '../types'; import { Course, Group, Basket, GroupType, SchedulerEvent } from '../types';
import axios from 'axios';
import { CASContext } from './CASProvider';
import { useSnackbar } from 'notistack'; 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 { interface CourseContext {
courses: Array<Course>; courses: Array<Course>;
basket: Array<Basket>; basket: Array<Basket>;
addToBasket: (courses: Course) => void; hoveredGroup: Group | undefined | null;
addGroup: (group: Group, id: number) => void; 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; deleteFromBasket: (id: number) => void;
saveBasket: () => 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); export const coursesContext = createContext<CourseContext | undefined>(undefined);
@ -19,28 +36,60 @@ interface CoursesProviderProps {
} }
export const CoursesProvider = ({ children }: 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 { enqueueSnackbar } = useSnackbar();
const { closeSnackbar } = useSnackbar(); const { closeSnackbar } = useSnackbar();
const CAS = useContext(CASContext)!; //fetch courses with groups
const token = CAS?.user?.token; const [courses, setCourses] = useState<Array<Course>>([]);
const [basket, setBasket] = useState<Array<Basket>>([]);
const selectBasketIds = (basket: Array<Basket>) => { const [hoveredGroup, setHoveredGroup] = useState<Group | undefined | null>(null);
const classesIds = basket.map((course) => course.classes.id); const selectBasketIds = () => {
const lecturesIds = basket.map((course) => course?.lecture?.id); 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 lecturesIds[0] === undefined ? classesIds : [...classesIds, ...lecturesIds]; 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 = { const courseToBasket: Basket = {
name: course.name, name: course.name,
id: course.id, id: course.id,
classes: course.classes[0], classes: course.classes !== undefined ? course.classes[0] : undefined,
lecture: course.lectures !== undefined ? course.lectures[0] : undefined, lecture: course.lectures !== undefined ? course.lectures[0] : undefined,
}; };
setBasket([...basket, courseToBasket]); setBasket([...basket, courseToBasket]);
@ -49,37 +98,25 @@ export const CoursesProvider = ({ children }: CoursesProviderProps) => {
const deleteFromBasket = (id: number) => setBasket(basket.filter((course) => course.id !== id)); const deleteFromBasket = (id: number) => setBasket(basket.filter((course) => course.id !== id));
const saveBasket = async () => { const saveBasket = async () => {
const basketIds = selectBasketIds(basket); const basketIds = selectBasketIds();
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 action = (key: any) => ( const action = (key: any) => (
<> <>
<button <StyledCloseIcon
onClick={() => { onClick={() => {
closeSnackbar(key); closeSnackbar(key);
}} }}
> ></StyledCloseIcon>
X
</button>
</> </>
); );
try { 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', { enqueueSnackbar('Plan został zapisany', {
variant: 'success', variant: 'success',
action, action,
}); });
} catch (e) { } catch (e) {
console.log('error: ', e);
enqueueSnackbar('Zapisywanie planu nie powiodło się', { enqueueSnackbar('Zapisywanie planu nie powiodło się', {
variant: 'error', variant: 'error',
action, action,
@ -87,8 +124,8 @@ export const CoursesProvider = ({ children }: CoursesProviderProps) => {
} }
}; };
const addGroup = (choosenGroup: Group, id: number) => { const changeGroupInBasket = (choosenGroup: Group, courseId: number) => {
const basketCourse = basket.filter((course) => course.id === id)[0]; const basketCourse = basket.filter((course) => course.id === courseId)[0];
const { type } = choosenGroup; const { type } = choosenGroup;
if (type === GroupType.CLASS) { if (type === GroupType.CLASS) {
setBasket( setBasket(
@ -99,48 +136,72 @@ export const CoursesProvider = ({ children }: CoursesProviderProps) => {
basket.map((basket) => (basket.id === basketCourse.id ? { ...basket, lecture: choosenGroup } : basket)), 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 getNewestTimetable = async () => {
const config = {
method: 'get' as const,
url: `${process.env.REACT_APP_API_URL}/api/v1/assignments/getCurrentAssignments`,
headers: {
Authorization: `Bearer ${token}`,
},
};
try { try {
let { data: basket } = await axios.request(config); const { data } = await axiosInstance.get(
if (basket === '') { `${process.env.REACT_APP_API_URL}/api/v1/assignments/user`,
basket = []; );
} const basket = data === '' ? [] : data;
setBasket(basket); setBasket(basket);
} catch (e) { } catch (e) {
console.log(e); console.log(e);
} }
}; };
const fetchClasses = async () => { const fetchCourses = async () => {
try { try {
const { data: courses } = await axios.get<Array<Course>>( const { data: courses } = await axiosInstance.get<Array<Course>>(
`${process.env.REACT_APP_API_URL}/api/v1/courses/getCoursesWithGroups`, `${process.env.REACT_APP_API_URL}/api/v1/courses/all?groups=true`,
); );
courses.sort((a, b) => (a.name > b.name ? 1 : -1)); const sortedCourses = courses.sort((a, b) => (a.name > b.name ? 1 : -1));
setCourses(courses); setCourses(sortedCourses);
} catch (e) { } catch (e) {
console.log(e); console.log(e);
} }
}; };
useEffect(() => { useEffect(() => {
fetchClasses(); setTimeout(() => {
if (token) { fetchCourses();
getNewestTimetable(); getNewestTimetable();
} }, 200);
}, [token]); }, []);
return ( 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} {children}
</coursesContext.Provider> </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; id: number;
name: string; name: string;
lecture?: Group; lecture?: Group;
classes: Group; classes?: Group;
} }
export interface Group { export interface Group {
@ -24,11 +24,21 @@ export interface Course {
id: number; id: number;
name: string; name: string;
lectures?: Array<Group>; lectures?: Array<Group>;
classes: Array<Group>; classes?: Array<Group>;
} }
export interface User { export interface User {
name?: string; name?: string;
surname?: 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