Added most of backend interactions and rewrote filers logic

This commit is contained in:
Dm1tr1y147 2020-09-18 23:57:35 +05:00
parent a83e667dae
commit 403c57cdba
11 changed files with 346 additions and 146 deletions

View File

@ -1,62 +1,98 @@
import React, { useState } from "react";
import { Switch, Route, useLocation } from "react-router-dom";
import React, { useEffect, useRef, useState } from 'react';
import { Switch, Route, useHistory } from 'react-router-dom';
import "./App.css";
import Home from "./Home";
import Navbar from "./Navbar";
import SubjectList from "./SubjectList";
import {ILoadingState} from './types'
import './App.css';
import Home from './Home';
import Navbar from './Navbar';
import { queryIsEmpty } from './Navbar/utils';
import SubjectList from './SubjectList';
import { ILoadingState, IFilterQuery } from './types';
const genName = (path: string, search?: string): string => {
if (path === "/list" && search) {
search = decodeURI(search);
let query: any = {};
search
.split("?")
.slice(1)
.map((param) => (query[param.split("=")[0]] = param.split("=")[1]));
const genName = (searchQuery: IFilterQuery, path: string): string => {
if (path === '/list' && searchQuery) {
let result = '';
let result = "";
if (query.clas) {
result = result + query.clas + " класс";
if (searchQuery.class_num) {
result = result + searchQuery.class_num + ' класс';
}
if (query.subject) {
result = result + ", " + query.subject;
if (searchQuery.predmet_type) {
result = result + ' ' + searchQuery.predmet_type;
}
if (query.teacher) {
result = result + ", " + query.teacher;
if (searchQuery.teacher) {
result = result + ' ' + searchQuery.teacher;
}
if (searchQuery.search) {
result = result + ' поиск по "' + searchQuery.search + '"';
}
return result;
}
if (path === "/") {
return "Банк семинаров";
if (path === '/') {
return 'Банк семинаров';
}
return "";
return '';
};
function App() {
const location = useLocation();
const useDidUpdate: typeof useEffect = (func, dependencies) => {
const didMountRef = useRef(false);
const [loading, setLoading] = useState<ILoadingState>({ fetching: true, error: "" });
useEffect(() => {
if (didMountRef.current) {
func();
} else {
didMountRef.current = true;
}
return () => {};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, dependencies);
};
const App = () => {
const history = useHistory();
const [loading, setLoading] = useState<ILoadingState>({
fetching: true,
error: ''
});
const [searchQuery, setSearchQuery] = useState<IFilterQuery>({
search: '',
class_num: '',
type_num: '',
predmet_type: '',
teacher: ''
});
useDidUpdate(() => {
if (queryIsEmpty(searchQuery)) return;
history.push('/list');
}, [searchQuery]);
return (
<div>
<header id="name">
<h1>{genName(location.pathname, location.search)}</h1>
<h1>{genName(searchQuery, history.location.pathname)}</h1>
</header>
<Navbar />
<Navbar query={searchQuery} setSearchQuery={setSearchQuery} />
<Switch>
<Route exact path="/">
<Home setLoading={setLoading} />
<Home
setLoading={setLoading}
setSearchQuery={setSearchQuery}
/>
</Route>
<Route path="/list">
<SubjectList setLoading={setLoading} />
<SubjectList
searchQuery={searchQuery}
setLoading={setLoading}
/>
</Route>
<Route path="*">
<h1>404</h1>
@ -64,6 +100,6 @@ function App() {
</Switch>
</div>
);
}
};
export default App;

View File

@ -1,12 +0,0 @@
import React from "react";
import './main.css'
const Card = (props) => (
<a className="card" href={ '/' + props.data.image.slice(props.data.image.indexOf('media'))}>
<h4 className="cardTitle">{props.data.title}</h4>
<h5 className="cardTeacher">{props.data.teacher}</h5>
</a>
);
export default Card;

16
src/Card/index.tsx Normal file
View File

@ -0,0 +1,16 @@
import React from 'react';
import { IData } from '../types';
import './main.css';
const Card = ({ data }: { data: IData }) => (
<a
className="card"
href={'/' + data.image.slice(data.image.indexOf('media'))}
>
<h4 className="cardTitle">{data.title}</h4>
<h5 className="cardTeacher">{data.teacher}</h5>
</a>
);
export default Card;

View File

@ -1,27 +1,28 @@
import React, { Dispatch, useEffect, useState } from "react";
import { Link } from "react-router-dom";
import React, { Dispatch, SetStateAction, useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import Card from "../Card";
import { IData, ILoadingState } from "../types";
import "./main.css";
import Card from '../Card';
import { IData, IFilterQuery, ILoadingState } from '../types';
import './main.css';
const Home = ({ setLoading }: {setLoading: Dispatch<ILoadingState>}) => {
const Home = ({
setLoading,
setSearchQuery
}: {
setLoading: Dispatch<SetStateAction<ILoadingState>>;
setSearchQuery: Dispatch<SetStateAction<IFilterQuery>>;
}) => {
const [data, setData] = useState<IData[]>([]);
useEffect(() => {
setLoading({ fetching: true, error: "" });
console.log("Loading data");
setLoading({ fetching: true, error: '' });
const requestURL =
"https://cors-anywhere.herokuapp.com/upml-bank.dmitriy.icu/api/cards";
const requestURL = 'https://upml-bank.dmitriy.icu/api/cards';
fetch(requestURL)
.then((res) => res.json())
.then((data) => {
console.log("Fetched data");
console.log(data);
setData(data);
setLoading({ fetching: false, error: "" });
setLoading({ fetching: false, error: '' });
})
.catch((err) => {
setLoading({ fetching: false, error: err });
@ -30,9 +31,17 @@ const Home = ({ setLoading }: {setLoading: Dispatch<ILoadingState>}) => {
}, [setLoading]);
const classes: number[] = [
...Array.from(new Set(data.map((el) => parseInt(el.class_num)).sort())),
...Array.from(new Set(data.map((el) => parseInt(el.class_num)).sort()))
];
const subjects: string[] = [...Array.from(new Set(data.map((el) => el.predmet_type).sort()))];
const subjects: string[] = [
...Array.from(new Set(data.map((el) => el.predmet_type).sort()))
];
const handleShowMore = (predmet_type: string, class_num: number) => {
setSearchQuery((prev): IFilterQuery => {
return { ...prev, predmet_type, class_num: class_num.toString() };
});
};
return (
<main className="homeContainer">
@ -62,12 +71,13 @@ const Home = ({ setLoading }: {setLoading: Dispatch<ILoadingState>}) => {
</div>
<div className="showMore">
<Link
to={
"/list?subject=" +
subject +
"?clas=" +
class_num
onClick={() =>
handleShowMore(
subject,
class_num
)
}
to={'/list'}
>
Больше &rarr;
</Link>
@ -75,7 +85,7 @@ const Home = ({ setLoading }: {setLoading: Dispatch<ILoadingState>}) => {
</div>
</div>
) : (
""
''
)
)}

View File

@ -1,57 +1,138 @@
import React, { useState } from "react";
import { motion } from "framer-motion";
import { Link } from "react-router-dom";
import React, {
ChangeEvent,
Dispatch,
SetStateAction,
useEffect,
useRef,
useState
} from 'react';
import { motion } from 'framer-motion';
import { Link } from 'react-router-dom';
import "./main.css";
import './main.css';
import LogoImage from "./logo.png";
import FilterIcon from "./filter.svg";
import SearchIcon from "./search.svg";
import LogoImage from './logo.png';
import FilterIcon from './filter.svg';
import SearchIcon from './search.svg';
import { IFilterQuery } from '../types';
import { emptyQuery } from './utils';
import { useFocus } from '../utils';
const Navbar = ({
setSearchQuery,
query
}: {
setSearchQuery: Dispatch<SetStateAction<IFilterQuery>>;
query: IFilterQuery;
}) => {
/*
* Hooks
*/
const Navbar = () => {
const [searchCollapsed, setSearchCollapsed] = useState(true);
const [filtersCollapsed, setFiltersCollapsed] = useState(true);
const [localFilters, setLocalFilters] = useState<Partial<IFilterQuery>>();
const searchInput = useRef<HTMLInputElement>(null);
const setInputFocus = useFocus(searchInput);
const formRef = useRef<HTMLFormElement>(null);
useEffect(() => {
if (formRef.current) {
for (const [key, value] of Object.entries(query)) {
console.log(key, value);
if (formRef.current.elements.namedItem(key)) {
(formRef.current.elements.namedItem(
key
) as HTMLSelectElement).value = value;
}
}
}
}, [query]);
/*
* Animations
*/
const searchVariants = {
open: {
width: "calc(100vw - 4vh)",
display: "block",
width: 'calc(100vw - 4vh)',
display: 'block'
},
closed: {
width: "6vh",
width: '6vh',
transitionEnd: {
display: "none",
},
},
display: 'none'
}
}
};
const navVariants = {
open: {
height: "100vh",
height: '100vh',
borderTopLeftRadius: 0,
borderTopRightRadius: 0,
borderTopRightRadius: 0
},
closed: {
height: "10vh",
borderTopLeftRadius: "20px",
borderTopRightRadius: "20px",
},
height: '10vh',
borderTopLeftRadius: '20px',
borderTopRightRadius: '20px'
}
};
const filtersVariants = {
open: {
height: "100vh",
padding: "2vh",
height: '100vh',
padding: '2vh'
},
closed: {
height: 0,
padding: 0,
},
padding: 0
}
};
const transition = {
ease: "easeIn",
duration: 0.5,
ease: 'easeIn',
duration: 0.5
};
/*
* Input handlers
*/
const handleFiltersButton = () => {
if (!filtersCollapsed) {
setSearchQuery((prev) => {
return { ...prev, ...localFilters };
});
}
setFiltersCollapsed((prev) => !prev);
};
const handleSelectChange = ({
target: element
}: ChangeEvent<HTMLSelectElement>) => {
setLocalFilters((prev) => {
return { ...prev, [element.name]: element.value };
});
};
const handleSearchButton = () => {
if (searchCollapsed) {
setSearchCollapsed(false);
setInputFocus();
} else if (searchInput && searchInput.current) {
const value = searchInput.current.value;
setSearchQuery((prev) => {
return { ...prev, search: value };
});
setSearchCollapsed(true);
searchInput.current.value = '';
}
};
return (
@ -59,10 +140,15 @@ const Navbar = () => {
id="navbar"
variants={navVariants}
transition={transition}
animate={filtersCollapsed ? "closed" : "open"}
animate={filtersCollapsed ? 'closed' : 'open'}
>
<nav>
<Link to="/">
<Link
to="/"
onClick={() => {
emptyQuery(setSearchQuery);
}}
>
<img id="logo" src={LogoImage} alt="Логотип ЮФМЛ" />
</Link>
@ -71,7 +157,7 @@ const Navbar = () => {
<button
className="navButton"
id="filter"
onClick={() => setFiltersCollapsed((prev) => !prev)}
onClick={handleFiltersButton}
>
<img src={FilterIcon} alt="Фильтр" />
</button>
@ -79,16 +165,17 @@ const Navbar = () => {
<button
className="navButton"
id="search"
onClick={() => setSearchCollapsed((prev) => !prev)}
onClick={handleSearchButton}
>
<img src={SearchIcon} alt="Поиск" />
</button>
<motion.input
type="search"
animate={searchCollapsed ? "closed" : "open"}
animate={searchCollapsed ? 'closed' : 'open'}
variants={searchVariants}
transition={transition}
aria-label="Поиск"
ref={searchInput}
type="search"
name="search"
id="searchInput"
/>
@ -97,17 +184,20 @@ const Navbar = () => {
<motion.form
variants={filtersVariants}
transition={transition}
animate={filtersCollapsed ? "closed" : "open"}
animate={filtersCollapsed ? 'closed' : 'open'}
id="filters"
ref={formRef}
>
<div>
<label htmlFor="teacherFilter">
<h2>Преподаватель</h2>
</label>
<select name="teacher" id="teacherFilter">
<option value="">
-
</option>
<select
name="teacher"
onChange={handleSelectChange}
id="teacherFilter"
>
<option value="">-</option>
<option value="Попов Д.А">Попов Д.А</option>
<option value="Ильин А.Б">Ильин А.Б</option>
<option value="Пачин И.М">Пачин И.М</option>
@ -127,10 +217,12 @@ const Navbar = () => {
<label htmlFor="typeFilter">
<h2>Тип задания</h2>
</label>
<select name="type_num" id="typeFilter">
<option value="">
-
</option>
<select
name="type_num"
onChange={handleSelectChange}
id="typeFilter"
>
<option value="">-</option>
<option value="Семестровки">Семестровки</option>
<option value="Семинары">Семинары</option>
<option value="Потоковые">Потоковые</option>
@ -140,10 +232,12 @@ const Navbar = () => {
<label htmlFor="predmetFilter">
<h2>Предмет</h2>
</label>
<select name="predmet_type" id="predmetFilter">
<option value="">
-
</option>
<select
name="predmet_type"
onChange={handleSelectChange}
id="predmetFilter"
>
<option value="">-</option>
<option value="Математика">Математика</option>
<option value="Физика">Физика</option>
<option value="Информатика">Информатика</option>
@ -153,10 +247,12 @@ const Navbar = () => {
<label htmlFor="classFilter">
<h2>Класс</h2>
</label>
<select name="class_num" id="classFilter">
<option value="">
-
</option>
<select
name="class_num"
onChange={handleSelectChange}
id="classFilter"
>
<option value="">-</option>
<option value="10">10</option>
<option value="11">11</option>
</select>

24
src/Navbar/utils.ts Normal file
View File

@ -0,0 +1,24 @@
import { Dispatch, SetStateAction } from 'react';
import { IFilterQuery } from '../types';
const queryIsEmpty = (q: IFilterQuery): boolean => {
for (const [_, value] of Object.entries(q)) {
if (value) {
return false;
}
}
return true;
};
const emptyQuery = (setter: Dispatch<SetStateAction<IFilterQuery>>) => {
setter({
class_num: '',
type_num: '',
predmet_type: '',
search: '',
teacher: ''
});
};
export { queryIsEmpty, emptyQuery };

View File

@ -1,20 +1,32 @@
import React, { useEffect, useState, Dispatch } from 'react';
import { useLocation } from 'react-router-dom';
import { useHistory } from 'react-router-dom';
import { IData, ILoadingState } from '../types';
import { IData, ILoadingState, IFilterQuery } from '../types';
import { queryIsEmpty } from '../Navbar/utils';
import Card from '../Card';
import './main.css';
const SubjectList = ({
setLoading
setLoading,
searchQuery
}: {
setLoading: Dispatch<ILoadingState>;
searchQuery: IFilterQuery;
}) => {
const [data, setData] = useState<IData[]>([]);
const history = useHistory();
useEffect(() => {
if (queryIsEmpty(searchQuery)) {
history.push('/');
return;
}
setLoading({ fetching: true, error: '' });
const fetchURL = 'https://upml-bank.dmitriy.icu/api/cards?' + new URLSearchParams({class_num: '11'}).toString()
const fetchURL =
'https://upml-bank.dmitriy.icu/api/cards?' +
new URLSearchParams({ ...searchQuery }).toString();
fetch(fetchURL)
.then((res) => res.json())
.then((data) => {
@ -25,7 +37,7 @@ const SubjectList = ({
console.error(err);
setLoading({ fetching: false, error: err });
});
}, [setLoading]);
}, [setLoading, searchQuery]);
return (
<main className="subjectList">

View File

@ -4,6 +4,7 @@
-ms-overflow-style: none;
scrollbar-width: none;
box-sizing: border-box;
outline: none;
}
*::-webkit-scrollbar {
@ -36,4 +37,7 @@ code {
#name h1 {
text-align: center;
line-height: 6vh;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}

View File

@ -15,4 +15,12 @@ interface IData {
card_id: number;
}
export type { ILoadingState, IData };
interface IFilterQuery {
class_num: string;
type_num: string;
predmet_type: string;
teacher: string;
search: string;
}
export type { ILoadingState, IData, IFilterQuery };

11
src/utils.ts Normal file
View File

@ -0,0 +1,11 @@
const useFocus = (focusRef: React.RefObject<HTMLInputElement>) => {
const setFocus = () => {
setTimeout(() => {
focusRef.current && focusRef.current.focus();
}, 500);
};
return setFocus;
};
export { useFocus };

View File

@ -1,25 +1,20 @@
{
"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"
]
"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",
"suppressImplicitAnyIndexErrors": true
},
"include": ["src"]
}