hihihi
2
.eslintignore
Normal file
@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
build
|
28
.eslintrc.js
Normal file
@ -0,0 +1,28 @@
|
||||
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',
|
||||
},
|
||||
},
|
||||
};
|
7
.prettierrc.js
Normal file
@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
semi: true,
|
||||
trailingComma: 'all',
|
||||
singleQuote: true,
|
||||
printWidth: 120,
|
||||
tabWidth: 2,
|
||||
};
|
14443
package-lock.json
generated
Normal file
46
package.json
Normal file
@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "plannaplan",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@material-ui/core": "^4.10.0",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.5.0",
|
||||
"@testing-library/user-event": "^7.2.1",
|
||||
"axios": "^0.19.2",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-scripts": "3.4.1",
|
||||
"styled-components": "^5.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^24.9.1",
|
||||
"@types/node": "^12.12.54",
|
||||
"@types/react": "^16.9.46",
|
||||
"@types/react-dom": "^16.9.8",
|
||||
"@types/styled-components": "^5.1.2",
|
||||
"@typescript-eslint/parser": "^3.9.1",
|
||||
"prettier": "^2.0.5",
|
||||
"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"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
13
public/manifest.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"short_name": "PlanNaPlan",
|
||||
"name": "PlanNaPlan",
|
||||
"icons": [
|
||||
{
|
||||
"src": "logo.svg"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
BIN
src/assets/PL.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
src/assets/UK.png
Normal file
After Width: | Height: | Size: 11 KiB |
1
src/assets/close.svg
Normal file
@ -0,0 +1 @@
|
||||
<?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>
|
After Width: | Height: | Size: 297 B |
BIN
src/assets/expand.png
Normal file
After Width: | Height: | Size: 535 B |
1
src/assets/search.svg
Normal file
@ -0,0 +1 @@
|
||||
<?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>
|
After Width: | Height: | Size: 808 B |
BIN
src/assets/transfer.png
Normal file
After Width: | Height: | Size: 6.2 KiB |
BIN
src/assets/user.png
Normal file
After Width: | Height: | Size: 18 KiB |
30
src/components/App.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import React, { useState, useContext } from 'react';
|
||||
import Topbar from './Topbar';
|
||||
import { Transfer } from './Transfer';
|
||||
import { Scheduler } from './Scheduler';
|
||||
import { Rightbar } from './Rightbar';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
export const App = () => {
|
||||
const [isOpenTransfer, setOpenTransfer] = useState(false);
|
||||
|
||||
|
||||
const handleTransfer = () => {
|
||||
setOpenTransfer(!isOpenTransfer);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Topbar handleTransfer={handleTransfer} />
|
||||
<Transfer isOpen={isOpenTransfer} handleClose={handleTransfer} />
|
||||
<Wrapper>
|
||||
<Scheduler />
|
||||
<Rightbar />
|
||||
</Wrapper>
|
||||
</>
|
||||
);
|
||||
};
|
103
src/components/CourseCard.tsx
Normal file
@ -0,0 +1,103 @@
|
||||
import React, { useContext, MouseEvent } from 'react';
|
||||
import Collapse from '@material-ui/core/Collapse';
|
||||
import ExpandIcon from '../assets/expand.png';
|
||||
import { Course, Group } from '../types/index';
|
||||
import { coursesContext } from '../contexts/CoursesProvider';
|
||||
import styled from 'styled-components';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
|
||||
interface ClassExandIconProps {
|
||||
isSelected: boolean;
|
||||
}
|
||||
|
||||
const CourseStyled = styled.div`
|
||||
display: flex;
|
||||
min-height: 50px;
|
||||
background-color: rgb(100, 181, 246) !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
margin-top: 10px;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
align-items: stretch;
|
||||
`;
|
||||
|
||||
const CourseNameStyled = styled.div`
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
`;
|
||||
|
||||
const ClassGroupStyled = styled.div`
|
||||
padding-top: 1px;
|
||||
padding-bottom: 1px;
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
transition: 1s;
|
||||
background-color: #8bc8fb;
|
||||
}
|
||||
`;
|
||||
|
||||
const ClassExandIconStyled = styled.img<ClassExandIconProps>`
|
||||
margin-top: 5px;
|
||||
width: 20px;
|
||||
transition: 0.2s;
|
||||
transform: ${(props) => (props.isSelected ? 'scaleY(-1);' : 'scaleY(1);')};
|
||||
`;
|
||||
|
||||
const useStyles = makeStyles({
|
||||
expanded: {
|
||||
maxHeight: '244px',
|
||||
overflowY: 'auto',
|
||||
},
|
||||
'@global': {
|
||||
'*::-webkit-scrollbar': {
|
||||
width: '0.4em',
|
||||
},
|
||||
'*::-webkit-scrollbar-track': {
|
||||
'-webkit-box-shadow': 'inset 0 0 6px rgba(1,0,0,0.1)',
|
||||
},
|
||||
'*::-webkit-scrollbar-thumb': {
|
||||
borderRadius: '10px',
|
||||
backgroundColor: '#d4b851',
|
||||
outline: '1px solid slategrey',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
interface CourseCardProps {
|
||||
onCardClick: (event: MouseEvent) => void;
|
||||
course: Course;
|
||||
id: string;
|
||||
isSelected: boolean;
|
||||
}
|
||||
|
||||
export const CourseCard = ({ onCardClick, course, id, isSelected }: CourseCardProps) => {
|
||||
const classes = useStyles();
|
||||
|
||||
const { addGroup } = useContext(coursesContext)!;
|
||||
|
||||
console.log(`course`);
|
||||
console.log(course);
|
||||
const onGroupClick = (group: Group, id: number) => addGroup(group, id);
|
||||
|
||||
return (
|
||||
<CourseStyled onClick={onCardClick} id={id}>
|
||||
<CourseNameStyled>{course.name}</CourseNameStyled>
|
||||
<Collapse className={classes.expanded} in={isSelected} timeout="auto" unmountOnExit>
|
||||
{course.groups.map((group, index) => (
|
||||
<ClassGroupStyled key={index} onClick={() => onGroupClick(group, course.id)}>
|
||||
<p>
|
||||
{group.time} {group.room} <br></br> {group.lecturer}
|
||||
</p>
|
||||
</ClassGroupStyled>
|
||||
))}
|
||||
</Collapse>
|
||||
<div onClick={onCardClick} id={id}>
|
||||
<ClassExandIconStyled isSelected={isSelected} alt="expand" src={ExpandIcon} />
|
||||
</div>
|
||||
</CourseStyled>
|
||||
);
|
||||
};
|
116
src/components/Dropdown.tsx
Normal file
@ -0,0 +1,116 @@
|
||||
import React, { useState, useContext, useEffect, MouseEvent } from 'react';
|
||||
import axios from 'axios';
|
||||
import { Input } from '@material-ui/core';
|
||||
import ClickAwayListener from '@material-ui/core/ClickAwayListener';
|
||||
import { coursesContext } from '../contexts/CoursesProvider';
|
||||
import { Course, Basket } from '../types';
|
||||
import styled from 'styled-components';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
|
||||
const CourseStyled = styled.div`
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
padding: 5px;
|
||||
padding-left: 20px;
|
||||
background-color: #e6c759;
|
||||
font-size: 18px;
|
||||
font-family: Lato;
|
||||
:hover {
|
||||
background-color: #d4b851;
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
||||
const DropdownStyled = styled.div`
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const useStyles = makeStyles({
|
||||
topbarInput: {
|
||||
marginTop: '8px',
|
||||
width: '100%',
|
||||
},
|
||||
});
|
||||
|
||||
interface DropdownProps {
|
||||
clearInput: boolean;
|
||||
handleClearInput: () => void;
|
||||
}
|
||||
|
||||
export const Dropdown = ({ clearInput, handleClearInput }: DropdownProps) => {
|
||||
const classes = useStyles();
|
||||
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [input, setInput] = useState<string>('');
|
||||
|
||||
//courses - choosenCourses
|
||||
const [filteredCourses, setFilteredCourses] = useState<Array<Course>>([]);
|
||||
|
||||
const { courses, basket, addToBasket } = useContext(coursesContext)!;
|
||||
|
||||
useEffect(() => {
|
||||
const filterCourses = (input: string) => {
|
||||
const choosenCoursesNames = basket.map(({ name }) => name.trim());
|
||||
const filteredCourses = courses.filter(
|
||||
({ name }) => name.toLowerCase().includes(input.toLowerCase()) && !choosenCoursesNames.includes(name),
|
||||
);
|
||||
setFilteredCourses(filteredCourses);
|
||||
};
|
||||
filterCourses(input);
|
||||
}, [input, open, basket]);
|
||||
|
||||
useEffect(() => {
|
||||
if (clearInput) {
|
||||
setInput('');
|
||||
handleClearInput();
|
||||
}
|
||||
}, [clearInput]);
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => setInput(event.target.value);
|
||||
|
||||
const handleClick = () => setOpen(true);
|
||||
|
||||
const handleClickAway = () => setOpen(false);
|
||||
|
||||
const onCourseClick = async (event: MouseEvent) => {
|
||||
const target = event.currentTarget;
|
||||
if (target.id && target.textContent) {
|
||||
const id = target.id;
|
||||
const name = target.textContent;
|
||||
|
||||
//porozmawiać z Filipem, żeby odrobinę przerobił endpoint
|
||||
const course: Basket = { name: name, id: parseInt(id), lecture: null, classes: null };
|
||||
|
||||
addToBasket(course);
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ClickAwayListener onClickAway={handleClickAway}>
|
||||
<div>
|
||||
<Input
|
||||
placeholder="Wyszukaj..."
|
||||
inputProps={{ 'aria-label': 'description' }}
|
||||
className={classes.topbarInput}
|
||||
onChange={handleChange}
|
||||
onClick={handleClick}
|
||||
value={input}
|
||||
/>
|
||||
{open && (
|
||||
<DropdownStyled>
|
||||
{filteredCourses.map(({ name, id }, index) => (
|
||||
<CourseStyled key={index} id={id.toString()} onClick={onCourseClick}>
|
||||
<p>{name} </p>
|
||||
</CourseStyled>
|
||||
))}
|
||||
</DropdownStyled>
|
||||
)}
|
||||
</div>
|
||||
</ClickAwayListener>
|
||||
);
|
||||
};
|
20
src/components/Profile.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import { Menu, MenuItem } from '@material-ui/core';
|
||||
import React, { useContext } from 'react';
|
||||
import { CASContext } from '../contexts/CASProvider';
|
||||
|
||||
interface ProfileProps {
|
||||
anchorEl: HTMLElement | null;
|
||||
handleClose: () => void;
|
||||
}
|
||||
|
||||
export const Profile = ({ anchorEl, handleClose }: ProfileProps) => {
|
||||
const { logout } = useContext(CASContext)!;
|
||||
|
||||
return (
|
||||
<Menu anchorEl={anchorEl} keepMounted open={Boolean(anchorEl)} onClose={handleClose}>
|
||||
<MenuItem>Profile</MenuItem>
|
||||
<MenuItem>My account</MenuItem>
|
||||
<MenuItem onClick={logout}>Logout</MenuItem>
|
||||
</Menu>
|
||||
);
|
||||
};
|
69
src/components/Rightbar.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import React, { useState, useContext, MouseEvent } from 'react';
|
||||
import { CourseCard } from './CourseCard';
|
||||
import { coursesContext } from '../contexts/CoursesProvider';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const RightbarStyled = styled.div`
|
||||
padding-top: 10px;
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
text-align: center;
|
||||
font-family: Lato;
|
||||
width: 300px;
|
||||
height: 85vh;
|
||||
overflow-y: scroll;
|
||||
::-webkit-scrollbar-track {
|
||||
border-radius: 10px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 10px;
|
||||
background-color: #d4b851;
|
||||
border: 1px solid;
|
||||
}
|
||||
`;
|
||||
const RightbarTextStyled = styled.div`
|
||||
border-bottom: 1px solid;
|
||||
`;
|
||||
|
||||
export const Rightbar = () => {
|
||||
const [selectedCardId, setSelectedCardId] = useState<string | null>(null);
|
||||
|
||||
const { courses, basket } = useContext(coursesContext)!;
|
||||
|
||||
const getBasketGroups = () => {
|
||||
const ids = basket.map(({ id }) => id);
|
||||
return courses.filter(({ id }) => ids.includes(id));
|
||||
};
|
||||
|
||||
const filteredCourses = getBasketGroups();
|
||||
|
||||
//działa clunky
|
||||
const onCardClick = (event: MouseEvent) => {
|
||||
const target = event.currentTarget;
|
||||
selectedCardId === target.id ? setSelectedCardId(null) : setSelectedCardId(target.id);
|
||||
};
|
||||
|
||||
//need to insert student name from db and course maybe based on current time or from db too
|
||||
return (
|
||||
<RightbarStyled>
|
||||
<RightbarTextStyled>
|
||||
Hubert Wrzesiński<br></br>
|
||||
Semestr zimowy 2020/2021
|
||||
</RightbarTextStyled>
|
||||
{filteredCourses.map((course, index) => (
|
||||
<CourseCard
|
||||
course={course}
|
||||
key={index}
|
||||
id={index.toString()}
|
||||
onCardClick={onCardClick}
|
||||
isSelected={selectedCardId === index.toString()}
|
||||
/>
|
||||
))}
|
||||
</RightbarStyled>
|
||||
);
|
||||
};
|
107
src/components/Scheduler.tsx
Normal file
@ -0,0 +1,107 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { SchedulerEvents } from './SchedulerEvents';
|
||||
import { days, hours } from '../constants/index';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const SchedulerWrapper = styled.div`
|
||||
flex-grow: 3;
|
||||
margin-top: 20px;
|
||||
border-collapse: collapse;
|
||||
`;
|
||||
|
||||
const TableBody = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const TableRow = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
`;
|
||||
|
||||
const TableCell = styled.div`
|
||||
border: 1px solid #ddd;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
const TableHead = styled.div`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const TableHeadCell = styled.div`
|
||||
border: 1px solid #ddd;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
export const Scheduler = () => {
|
||||
const [currentEventsIds, setCurrentEventsIds] = useState<Array<string>>([]);
|
||||
const cellRef = useRef<HTMLDivElement>(null);
|
||||
const [cellWidth, setCellWidth] = useState(0);
|
||||
const [cellTop, setCellTop] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (cellRef.current) {
|
||||
setCellWidth(cellRef.current.getBoundingClientRect().width);
|
||||
setCellTop(cellRef.current.getBoundingClientRect().top);
|
||||
}
|
||||
};
|
||||
handleResize();
|
||||
window.addEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const displayEvents = () => {
|
||||
currentEventsIds.map((eventId: string) => {
|
||||
const event = document.getElementById(eventId);
|
||||
if (event) {
|
||||
event.style.display = 'block';
|
||||
}
|
||||
});
|
||||
};
|
||||
displayEvents();
|
||||
}, [currentEventsIds]);
|
||||
|
||||
// const handleClick = (e: React.MouseEvent) => {
|
||||
// const cellId = e.currentTarget.id;
|
||||
// const column = cellId.slice(0, 1);
|
||||
// const row = cellId.slice(1);
|
||||
// const eventId = `eventCol${column}eventRow${Math.floor(parseInt(row) / 2)}`;
|
||||
|
||||
// setCurrentEventsIds((currentEventsIds) => [...currentEventsIds, eventId]);
|
||||
// };
|
||||
|
||||
return (
|
||||
<>
|
||||
<SchedulerWrapper>
|
||||
<TableHead>
|
||||
{days.map((day, index) => (
|
||||
<TableHeadCell key={index}>{day}</TableHeadCell>
|
||||
))}
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{hours.map((hour, indexRow) => (
|
||||
<TableRow key={indexRow}>
|
||||
{[hour, '', '', '', '', ''].map((value, indexCell) =>
|
||||
indexRow === 0 && indexCell === 1 ? (
|
||||
<TableCell key={`${indexRow}${indexCell}`} ref={cellRef}></TableCell>
|
||||
) : (
|
||||
<TableCell key={`${indexRow}${indexCell}`}>{value}</TableCell>
|
||||
),
|
||||
)}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
<SchedulerEvents cellTop={cellTop} cellWidth={cellWidth} />
|
||||
</SchedulerWrapper>
|
||||
</>
|
||||
);
|
||||
};
|
72
src/components/SchedulerEvents.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { SchedulerRow } from './SchedulerRow';
|
||||
import { coursesContext } from '../contexts/CoursesProvider';
|
||||
import { Group, Basket } from '../types';
|
||||
|
||||
interface SchedulerEventsProps {
|
||||
cellTop: number;
|
||||
cellWidth: number;
|
||||
}
|
||||
|
||||
export const SchedulerEvents = ({ cellTop, cellWidth }: SchedulerEventsProps) => {
|
||||
const { basket } = useContext(coursesContext)!;
|
||||
|
||||
const [choosenGroupsMappedToEvents, setChoosenGroupsMappedToEvents] = useState<any>([]);
|
||||
|
||||
interface GroupTimeToEventRowMapping {
|
||||
[time: string]: number;
|
||||
}
|
||||
const groupTimeToEventRowMapping: GroupTimeToEventRowMapping = {
|
||||
'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 basketGroups = basket.map(({ classes, lecture }) => ({
|
||||
...classes,
|
||||
...lecture,
|
||||
})) as Array<Group>;
|
||||
|
||||
console.log('passed basket');
|
||||
console.log(basket);
|
||||
console.log(`basketgroups`);
|
||||
console.log(basketGroups);
|
||||
const groupsMapped = basketGroups.map(({ id, day, lecturer, room, time }) => ({
|
||||
id,
|
||||
day,
|
||||
lecturer,
|
||||
room,
|
||||
eventRow: groupTimeToEventRowMapping[time],
|
||||
}));
|
||||
setChoosenGroupsMappedToEvents(groupsMapped);
|
||||
}
|
||||
mapGroupTimeToEventRow(basket);
|
||||
}, [basket]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{[...Array(6)].map((_, index) => (
|
||||
<SchedulerRow
|
||||
key={index}
|
||||
groups={choosenGroupsMappedToEvents.filter((group: any) => {
|
||||
return group.eventRow === index;
|
||||
})}
|
||||
indexRow={index}
|
||||
cellTop={
|
||||
index == 3
|
||||
? cellTop + (25 + 80 * index)
|
||||
: index < 3
|
||||
? cellTop + (12 + 80 * index)
|
||||
: cellTop + (25 + 80 * index)
|
||||
}
|
||||
cellWidth={cellWidth}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
48
src/components/SchedulerRow.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import { Group } from '../types';
|
||||
import styled from 'styled-components';
|
||||
|
||||
interface SchedulerEventProps {
|
||||
eventIndex: number;
|
||||
cellTop: number;
|
||||
cellWidth: number;
|
||||
}
|
||||
|
||||
const SchedulerEvent = styled.div<SchedulerEventProps>`
|
||||
position: absolute;
|
||||
top: ${(props) => props.cellTop}px;
|
||||
left: ${(props) => props.cellWidth + 5 + props.cellWidth * props.eventIndex}px;
|
||||
width: ${(props) => (props.cellWidth * 2) / 3}px;
|
||||
height: 69px;
|
||||
background-color: lightblue;
|
||||
z-index: 2;
|
||||
`;
|
||||
|
||||
interface SchedulerRowProps {
|
||||
groups: Array<Group>;
|
||||
indexRow: number;
|
||||
cellTop: number;
|
||||
cellWidth: number;
|
||||
}
|
||||
|
||||
export const SchedulerRow = ({ groups, indexRow, cellTop, cellWidth }: SchedulerRowProps) => {
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
{[...Array(5)].map((_, eventIndex) => (
|
||||
<SchedulerEvent
|
||||
eventIndex={eventIndex}
|
||||
cellTop={cellTop}
|
||||
cellWidth={cellWidth}
|
||||
key={eventIndex}
|
||||
id={`eventRow${indexRow}eventCol${eventIndex}`}
|
||||
>
|
||||
{groups.map((group, index) =>
|
||||
group.day === eventIndex && <div key={index}>{groups[index]?.lecturer}</div>,
|
||||
)}
|
||||
</SchedulerEvent>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
117
src/components/Topbar.tsx
Normal file
@ -0,0 +1,117 @@
|
||||
import React, { useState, MouseEvent } from 'react';
|
||||
import Transfer from '../assets/transfer.png';
|
||||
import Search from '../assets/search.svg';
|
||||
import UK from '../assets/UK.png';
|
||||
import PL from '../assets/PL.png';
|
||||
import User from '../assets/user.png';
|
||||
import CloseIcon from '../assets/close.svg';
|
||||
import { Profile } from './Profile';
|
||||
import { Dropdown } from './Dropdown';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const TopbarTextStyled = styled.div`
|
||||
@media only screen and (max-width: 670px) {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const Topbar = styled.div`
|
||||
background-color: #ffdc61;
|
||||
height: 80px;
|
||||
padding: 5px;
|
||||
font-family: comic sans MS;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
const TopbarLogoWrapperStyled = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-grow: 0.5;
|
||||
justify-content: flex-start;
|
||||
`;
|
||||
|
||||
const TopbarLogoStyled = styled.img`
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
@media only screen and (max-width: 670px) {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
`;
|
||||
|
||||
const TopbarInputStyled = styled.div`
|
||||
width: 70%;
|
||||
display: flex;
|
||||
flex-grow: 3;
|
||||
`;
|
||||
|
||||
const TopbarInputFieldStyled = styled.div`
|
||||
width: 96%;
|
||||
margin-top: 10px;
|
||||
`;
|
||||
|
||||
const TopbarInputIconStyled = styled.img`
|
||||
width: 35px;
|
||||
@media only screen and (max-width: 670px) {
|
||||
width: 25px;
|
||||
}
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
const TopbarIcon = styled.img`
|
||||
width: 50px;
|
||||
cursor: pointer;
|
||||
@media only screen and (max-width: 670px) {
|
||||
width: 35px;
|
||||
}
|
||||
`;
|
||||
|
||||
const TopbarIconBox = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
flex-grow: 1.5;
|
||||
`;
|
||||
|
||||
interface TopbarProps {
|
||||
handleTransfer: (e: MouseEvent) => void;
|
||||
}
|
||||
|
||||
export default function ({ handleTransfer }: TopbarProps) {
|
||||
const [clearInput, setClearInput] = useState(false);
|
||||
const [isPolish, setIsPolish] = useState(false);
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLImageElement | null>(null);
|
||||
|
||||
const onLangChange = () => setIsPolish(!isPolish);
|
||||
|
||||
const handleProfile = (event: MouseEvent<HTMLImageElement>) => setAnchorEl(event.currentTarget);
|
||||
|
||||
const handleClose = () => setAnchorEl(null);
|
||||
|
||||
const handleClearInput = () => setClearInput(!clearInput);
|
||||
|
||||
return (
|
||||
<Topbar>
|
||||
<TopbarLogoWrapperStyled>
|
||||
<TopbarLogoStyled alt="logo" src="https://plannaplan.pl/img/logo.svg" />
|
||||
<TopbarTextStyled> plan na plan </TopbarTextStyled>
|
||||
</TopbarLogoWrapperStyled>
|
||||
<TopbarInputStyled>
|
||||
<TopbarInputIconStyled alt="search" src={Search} />
|
||||
<TopbarInputFieldStyled>
|
||||
<Dropdown clearInput={clearInput} handleClearInput={handleClearInput}/>
|
||||
</TopbarInputFieldStyled>
|
||||
<TopbarInputIconStyled alt="close" src={CloseIcon} onClick={handleClearInput}/>
|
||||
</TopbarInputStyled>
|
||||
<TopbarIconBox>
|
||||
<TopbarIcon alt="transfer" src={Transfer} onClick={handleTransfer} />
|
||||
<TopbarIcon alt="change_language" src={isPolish ? UK : PL} onClick={onLangChange} />
|
||||
<TopbarIcon alt="profile" src={User} onClick={handleProfile} />
|
||||
<Profile anchorEl={anchorEl} handleClose={handleClose} />
|
||||
</TopbarIconBox>
|
||||
</Topbar>
|
||||
);
|
||||
};
|
115
src/components/Transfer.tsx
Normal file
@ -0,0 +1,115 @@
|
||||
import React from 'react';
|
||||
import Modal from '@material-ui/core/Modal';
|
||||
import Fade from '@material-ui/core/Fade';
|
||||
import Input from '@material-ui/core/Input';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import styled from 'styled-components';
|
||||
|
||||
interface TransferProps {
|
||||
handleClose: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles({
|
||||
wrapper: {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
textAlign: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
const TransferStyled = styled.div`
|
||||
display: flex;
|
||||
|
||||
flex-direction: row;
|
||||
|
||||
outline: none;
|
||||
min-width: 35%;
|
||||
height: 70%;
|
||||
padding-top: 40px;
|
||||
background: #006b96;
|
||||
box-shadow: 0px 0px 0px 4px #006b96;
|
||||
border: 4px solid #ffc400;
|
||||
margin: 0 auto;
|
||||
border-top-left-radius: 5px;
|
||||
border-bottom-right-radius: 5px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3ch;
|
||||
`;
|
||||
|
||||
const TransferGiveStyled = styled.div`
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const TransferReceiveStyled = styled.div`
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const TransferTextStyled = styled.div`
|
||||
font-family: Lato;
|
||||
font-size: 30px;
|
||||
margin-bottom: 10px;
|
||||
`;
|
||||
|
||||
const TransferInputStyled = styled.div`
|
||||
width: 250px;
|
||||
height: 25px;
|
||||
padding: 10px;
|
||||
font-size: 24px;
|
||||
transition-duration: 0.3s;
|
||||
input::placeholder {
|
||||
color: black;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Transfer = ({ handleClose, isOpen }: TransferProps) => {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Modal
|
||||
className={classes.wrapper}
|
||||
open={isOpen}
|
||||
onClose={handleClose}
|
||||
aria-labelledby="simple-modal-title"
|
||||
aria-describedby="simple-modal-description"
|
||||
>
|
||||
<Fade in={isOpen}>
|
||||
<TransferStyled>
|
||||
<TransferGiveStyled>
|
||||
<TransferTextStyled>Oddam</TransferTextStyled>
|
||||
<TransferInputStyled>
|
||||
{' '}
|
||||
<Input
|
||||
placeholder="Wyszukaj..."
|
||||
inputProps={{ 'aria-label': 'description' }}
|
||||
className="top-bar__input-field"
|
||||
/>
|
||||
</TransferInputStyled>
|
||||
</TransferGiveStyled>
|
||||
<TransferReceiveStyled>
|
||||
<TransferTextStyled>Przyjmę</TransferTextStyled>
|
||||
<TransferInputStyled>
|
||||
{' '}
|
||||
<Input
|
||||
placeholder="Wyszukaj..."
|
||||
inputProps={{ 'aria-label': 'description' }}
|
||||
className="top-bar__input-field"
|
||||
/>
|
||||
</TransferInputStyled>
|
||||
</TransferReceiveStyled>
|
||||
</TransferStyled>
|
||||
</Fade>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
22
src/constants/index.ts
Normal file
@ -0,0 +1,22 @@
|
||||
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",
|
||||
];
|
47
src/contexts/CASProvider.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { User } from '../types';
|
||||
|
||||
export interface CASContext {
|
||||
user: User | null;
|
||||
logout: () => void;
|
||||
}
|
||||
|
||||
export const CASContext = React.createContext<CASContext | null>(null);
|
||||
|
||||
export interface CASProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const CASProvider = ({ children }: CASProviderProps) => {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
login();
|
||||
}, []);
|
||||
|
||||
function login() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const ticket = urlParams.get('ticket');
|
||||
|
||||
if (!ticket) {
|
||||
redirectToCASLoginService();
|
||||
}
|
||||
if (ticket) {
|
||||
setUser({ ...user, ticket: ticket });
|
||||
}
|
||||
}
|
||||
|
||||
function logout() {
|
||||
redirectToCASLogoutService();
|
||||
}
|
||||
|
||||
function redirectToCASLogoutService() {
|
||||
window.location.replace(`https://cas.amu.edu.pl/cas/logout?service=${window.origin}`);
|
||||
}
|
||||
|
||||
function redirectToCASLoginService() {
|
||||
window.location.replace(`https://cas.amu.edu.pl/cas/login?service=${window.origin}&locale=pl`);
|
||||
}
|
||||
|
||||
return <CASContext.Provider value={{ user, logout }}>{children}</CASContext.Provider>;
|
||||
};
|
63
src/contexts/CoursesProvider.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
import React, { useState, createContext, useEffect } from 'react';
|
||||
import { Course, Group, Basket, GroupType } from '../types';
|
||||
import axios from 'axios';
|
||||
|
||||
interface CourseContext {
|
||||
courses: Array<Course>;
|
||||
basket: Array<Basket>;
|
||||
addToBasket: (courses: Basket) => void;
|
||||
addGroup: (group: Group, id: number) => void;
|
||||
}
|
||||
export const coursesContext = createContext<CourseContext | null>(null);
|
||||
|
||||
interface CoursesProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const CoursesProvider = ({ children }: CoursesProviderProps) => {
|
||||
//fetch courses with groups
|
||||
const [courses, setCourses] = useState<Array<Course>>([]);
|
||||
const [basket, setBasket] = useState<Array<Basket>>([]);
|
||||
|
||||
const addToBasket = (course: Basket) => setBasket([...basket, course]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('BASKET');
|
||||
console.log(basket);
|
||||
}, [basket]);
|
||||
|
||||
//immutability
|
||||
|
||||
const addGroup = (choosenGroup: Group, id: number) => {
|
||||
const basketCourse = basket.filter((course) => course.id === id)[0];
|
||||
const type = choosenGroup.type;
|
||||
if (type === GroupType.CLASS) {
|
||||
setBasket(
|
||||
basket.map((basket) => (basket.id === basketCourse.id ? { ...basket, classes: choosenGroup } : basket)),
|
||||
);
|
||||
} else if (type === GroupType.LECTURE) {
|
||||
setBasket(
|
||||
basket.map((basket) => (basket.id === basketCourse.id ? { ...basket, lecture: choosenGroup } : basket)),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const { data: courses } = await axios.get(`${process.env.REACT_APP_API_URL}/getCourses`);
|
||||
|
||||
for (const course of courses) {
|
||||
const { data: groups } = await axios.get(`${process.env.REACT_APP_API_URL}/getCourseGroups?id=${course.id}`);
|
||||
//porozmawiać z Filipem, żeby odrobinę przerobił endpoint
|
||||
course.groups = groups;
|
||||
}
|
||||
|
||||
setCourses(courses);
|
||||
};
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<coursesContext.Provider value={{ courses, basket, addToBasket, addGroup }}>{children}</coursesContext.Provider>
|
||||
);
|
||||
};
|
29
src/contexts/reducers.ts
Normal file
@ -0,0 +1,29 @@
|
||||
// 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
|
18
src/index.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { App } from './components/App';
|
||||
import { CASProvider } from './contexts/CASProvider';
|
||||
import { CoursesProvider } from './contexts/CoursesProvider';
|
||||
import { GlobalStyles } from './styles/GlobalStyles';
|
||||
|
||||
ReactDOM.render(
|
||||
<>
|
||||
<CoursesProvider>
|
||||
<CASProvider>
|
||||
<GlobalStyles />
|
||||
<App />
|
||||
</CASProvider>
|
||||
</CoursesProvider>
|
||||
</>,
|
||||
document.getElementById('root'),
|
||||
);
|
20
src/styles/GlobalStyles.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { createGlobalStyle } from 'styled-components';
|
||||
|
||||
export const GlobalStyles = createGlobalStyle`
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
line-height: 24px;
|
||||
|
||||
}
|
||||
|
||||
body::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
`;
|
33
src/types/index.ts
Normal file
@ -0,0 +1,33 @@
|
||||
export enum GroupType {
|
||||
LECTURE = 'LECTURE',
|
||||
CLASS = 'CLASS',
|
||||
}
|
||||
|
||||
export interface Basket {
|
||||
id: number;
|
||||
name: string;
|
||||
lecture: Group | null;
|
||||
classes: Group | null;
|
||||
}
|
||||
|
||||
export interface Group {
|
||||
id: number;
|
||||
day: number;
|
||||
time: string;
|
||||
lecturer: string;
|
||||
room: string;
|
||||
type: GroupType;
|
||||
capacity?: number;
|
||||
}
|
||||
|
||||
export interface Course {
|
||||
id: number;
|
||||
name: string;
|
||||
groups: Array<Group>;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
name?: string;
|
||||
surname?: string;
|
||||
ticket: string | null;
|
||||
}
|
19
tsconfig.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|