This commit is contained in:
Maciek Głowacki 2020-08-26 17:01:25 +02:00
commit 796c641512
32 changed files with 15590 additions and 0 deletions

1
.env Normal file
View File

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

2
.eslintignore Normal file
View File

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

28
.eslintrc.js Normal file
View 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
View File

@ -0,0 +1,7 @@
module.exports = {
semi: true,
trailingComma: 'all',
singleQuote: true,
printWidth: 120,
tabWidth: 2,
};

14443
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

46
package.json Normal file
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
src/assets/UK.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

1
src/assets/close.svg Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 535 B

1
src/assets/search.svg Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

BIN
src/assets/user.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

30
src/components/App.tsx Normal file
View 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>
</>
);
};

View 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
View 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>
);
};

View 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>
);
};

View 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>
);
};

View 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>
</>
);
};

View 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>
);
};

View 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
View 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
View 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
View 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",
];

View 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>;
};

View 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
View 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
View 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'),
);

View 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
View 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
View 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"]
}