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 React, { useEffect, useRef, useState } from 'react';
import { Switch, Route, useLocation } from "react-router-dom"; import { Switch, Route, useHistory } from 'react-router-dom';
import "./App.css"; import './App.css';
import Home from "./Home"; import Home from './Home';
import Navbar from "./Navbar"; import Navbar from './Navbar';
import SubjectList from "./SubjectList"; import { queryIsEmpty } from './Navbar/utils';
import {ILoadingState} from './types' import SubjectList from './SubjectList';
import { ILoadingState, IFilterQuery } from './types';
const genName = (path: string, search?: string): string => { const genName = (searchQuery: IFilterQuery, path: string): string => {
if (path === "/list" && search) { if (path === '/list' && searchQuery) {
search = decodeURI(search); let result = '';
let query: any = {};
search
.split("?")
.slice(1)
.map((param) => (query[param.split("=")[0]] = param.split("=")[1]));
let result = ""; if (searchQuery.class_num) {
result = result + searchQuery.class_num + ' класс';
if (query.clas) {
result = result + query.clas + " класс";
} }
if (query.subject) { if (searchQuery.predmet_type) {
result = result + ", " + query.subject; result = result + ' ' + searchQuery.predmet_type;
} }
if (query.teacher) { if (searchQuery.teacher) {
result = result + ", " + query.teacher; result = result + ' ' + searchQuery.teacher;
}
if (searchQuery.search) {
result = result + ' поиск по "' + searchQuery.search + '"';
} }
return result; return result;
} }
if (path === "/") { if (path === '/') {
return "Банк семинаров"; return 'Банк семинаров';
} }
return ""; return '';
}; };
function App() { const useDidUpdate: typeof useEffect = (func, dependencies) => {
const location = useLocation(); 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 ( return (
<div> <div>
<header id="name"> <header id="name">
<h1>{genName(location.pathname, location.search)}</h1> <h1>{genName(searchQuery, history.location.pathname)}</h1>
</header> </header>
<Navbar /> <Navbar query={searchQuery} setSearchQuery={setSearchQuery} />
<Switch> <Switch>
<Route exact path="/"> <Route exact path="/">
<Home setLoading={setLoading} /> <Home
setLoading={setLoading}
setSearchQuery={setSearchQuery}
/>
</Route> </Route>
<Route path="/list"> <Route path="/list">
<SubjectList setLoading={setLoading} /> <SubjectList
searchQuery={searchQuery}
setLoading={setLoading}
/>
</Route> </Route>
<Route path="*"> <Route path="*">
<h1>404</h1> <h1>404</h1>
@ -64,6 +100,6 @@ function App() {
</Switch> </Switch>
</div> </div>
); );
} };
export default App; 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 React, { Dispatch, SetStateAction, useEffect, useState } from 'react';
import { Link } from "react-router-dom"; import { Link } from 'react-router-dom';
import Card from "../Card"; import Card from '../Card';
import { IData, ILoadingState } from "../types"; import { IData, IFilterQuery, ILoadingState } from '../types';
import "./main.css"; 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[]>([]); const [data, setData] = useState<IData[]>([]);
useEffect(() => { useEffect(() => {
setLoading({ fetching: true, error: "" }); setLoading({ fetching: true, error: '' });
console.log("Loading data");
const requestURL = const requestURL = 'https://upml-bank.dmitriy.icu/api/cards';
"https://cors-anywhere.herokuapp.com/upml-bank.dmitriy.icu/api/cards";
fetch(requestURL) fetch(requestURL)
.then((res) => res.json()) .then((res) => res.json())
.then((data) => { .then((data) => {
console.log("Fetched data");
console.log(data);
setData(data); setData(data);
setLoading({ fetching: false, error: "" }); setLoading({ fetching: false, error: '' });
}) })
.catch((err) => { .catch((err) => {
setLoading({ fetching: false, error: err }); setLoading({ fetching: false, error: err });
@ -30,9 +31,17 @@ const Home = ({ setLoading }: {setLoading: Dispatch<ILoadingState>}) => {
}, [setLoading]); }, [setLoading]);
const classes: number[] = [ 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 ( return (
<main className="homeContainer"> <main className="homeContainer">
@ -62,12 +71,13 @@ const Home = ({ setLoading }: {setLoading: Dispatch<ILoadingState>}) => {
</div> </div>
<div className="showMore"> <div className="showMore">
<Link <Link
to={ onClick={() =>
"/list?subject=" + handleShowMore(
subject + subject,
"?clas=" + class_num
class_num )
} }
to={'/list'}
> >
Больше &rarr; Больше &rarr;
</Link> </Link>
@ -75,7 +85,7 @@ const Home = ({ setLoading }: {setLoading: Dispatch<ILoadingState>}) => {
</div> </div>
</div> </div>
) : ( ) : (
"" ''
) )
)} )}

View File

@ -1,57 +1,138 @@
import React, { useState } from "react"; import React, {
import { motion } from "framer-motion"; ChangeEvent,
import { Link } from "react-router-dom"; 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 LogoImage from './logo.png';
import FilterIcon from "./filter.svg"; import FilterIcon from './filter.svg';
import SearchIcon from "./search.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 [searchCollapsed, setSearchCollapsed] = useState(true);
const [filtersCollapsed, setFiltersCollapsed] = 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 = { const searchVariants = {
open: { open: {
width: "calc(100vw - 4vh)", width: 'calc(100vw - 4vh)',
display: "block", display: 'block'
}, },
closed: { closed: {
width: "6vh", width: '6vh',
transitionEnd: { transitionEnd: {
display: "none", display: 'none'
}, }
}, }
}; };
const navVariants = { const navVariants = {
open: { open: {
height: "100vh", height: '100vh',
borderTopLeftRadius: 0, borderTopLeftRadius: 0,
borderTopRightRadius: 0, borderTopRightRadius: 0
}, },
closed: { closed: {
height: "10vh", height: '10vh',
borderTopLeftRadius: "20px", borderTopLeftRadius: '20px',
borderTopRightRadius: "20px", borderTopRightRadius: '20px'
}, }
}; };
const filtersVariants = { const filtersVariants = {
open: { open: {
height: "100vh", height: '100vh',
padding: "2vh", padding: '2vh'
}, },
closed: { closed: {
height: 0, height: 0,
padding: 0, padding: 0
}, }
}; };
const transition = { const transition = {
ease: "easeIn", ease: 'easeIn',
duration: 0.5, 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 ( return (
@ -59,10 +140,15 @@ const Navbar = () => {
id="navbar" id="navbar"
variants={navVariants} variants={navVariants}
transition={transition} transition={transition}
animate={filtersCollapsed ? "closed" : "open"} animate={filtersCollapsed ? 'closed' : 'open'}
> >
<nav> <nav>
<Link to="/"> <Link
to="/"
onClick={() => {
emptyQuery(setSearchQuery);
}}
>
<img id="logo" src={LogoImage} alt="Логотип ЮФМЛ" /> <img id="logo" src={LogoImage} alt="Логотип ЮФМЛ" />
</Link> </Link>
@ -71,7 +157,7 @@ const Navbar = () => {
<button <button
className="navButton" className="navButton"
id="filter" id="filter"
onClick={() => setFiltersCollapsed((prev) => !prev)} onClick={handleFiltersButton}
> >
<img src={FilterIcon} alt="Фильтр" /> <img src={FilterIcon} alt="Фильтр" />
</button> </button>
@ -79,16 +165,17 @@ const Navbar = () => {
<button <button
className="navButton" className="navButton"
id="search" id="search"
onClick={() => setSearchCollapsed((prev) => !prev)} onClick={handleSearchButton}
> >
<img src={SearchIcon} alt="Поиск" /> <img src={SearchIcon} alt="Поиск" />
</button> </button>
<motion.input <motion.input
type="search" animate={searchCollapsed ? 'closed' : 'open'}
animate={searchCollapsed ? "closed" : "open"}
variants={searchVariants} variants={searchVariants}
transition={transition} transition={transition}
aria-label="Поиск" aria-label="Поиск"
ref={searchInput}
type="search"
name="search" name="search"
id="searchInput" id="searchInput"
/> />
@ -97,17 +184,20 @@ const Navbar = () => {
<motion.form <motion.form
variants={filtersVariants} variants={filtersVariants}
transition={transition} transition={transition}
animate={filtersCollapsed ? "closed" : "open"} animate={filtersCollapsed ? 'closed' : 'open'}
id="filters" id="filters"
ref={formRef}
> >
<div> <div>
<label htmlFor="teacherFilter"> <label htmlFor="teacherFilter">
<h2>Преподаватель</h2> <h2>Преподаватель</h2>
</label> </label>
<select name="teacher" id="teacherFilter"> <select
<option value=""> name="teacher"
- onChange={handleSelectChange}
</option> id="teacherFilter"
>
<option value="">-</option>
<option value="Попов Д.А">Попов Д.А</option> <option value="Попов Д.А">Попов Д.А</option>
<option value="Ильин А.Б">Ильин А.Б</option> <option value="Ильин А.Б">Ильин А.Б</option>
<option value="Пачин И.М">Пачин И.М</option> <option value="Пачин И.М">Пачин И.М</option>
@ -127,10 +217,12 @@ const Navbar = () => {
<label htmlFor="typeFilter"> <label htmlFor="typeFilter">
<h2>Тип задания</h2> <h2>Тип задания</h2>
</label> </label>
<select name="type_num" id="typeFilter"> <select
<option value=""> name="type_num"
- onChange={handleSelectChange}
</option> id="typeFilter"
>
<option value="">-</option>
<option value="Семестровки">Семестровки</option> <option value="Семестровки">Семестровки</option>
<option value="Семинары">Семинары</option> <option value="Семинары">Семинары</option>
<option value="Потоковые">Потоковые</option> <option value="Потоковые">Потоковые</option>
@ -140,10 +232,12 @@ const Navbar = () => {
<label htmlFor="predmetFilter"> <label htmlFor="predmetFilter">
<h2>Предмет</h2> <h2>Предмет</h2>
</label> </label>
<select name="predmet_type" id="predmetFilter"> <select
<option value=""> name="predmet_type"
- onChange={handleSelectChange}
</option> id="predmetFilter"
>
<option value="">-</option>
<option value="Математика">Математика</option> <option value="Математика">Математика</option>
<option value="Физика">Физика</option> <option value="Физика">Физика</option>
<option value="Информатика">Информатика</option> <option value="Информатика">Информатика</option>
@ -153,10 +247,12 @@ const Navbar = () => {
<label htmlFor="classFilter"> <label htmlFor="classFilter">
<h2>Класс</h2> <h2>Класс</h2>
</label> </label>
<select name="class_num" id="classFilter"> <select
<option value=""> name="class_num"
- onChange={handleSelectChange}
</option> id="classFilter"
>
<option value="">-</option>
<option value="10">10</option> <option value="10">10</option>
<option value="11">11</option> <option value="11">11</option>
</select> </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 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 Card from '../Card';
import './main.css'; import './main.css';
const SubjectList = ({ const SubjectList = ({
setLoading setLoading,
searchQuery
}: { }: {
setLoading: Dispatch<ILoadingState>; setLoading: Dispatch<ILoadingState>;
searchQuery: IFilterQuery;
}) => { }) => {
const [data, setData] = useState<IData[]>([]); const [data, setData] = useState<IData[]>([]);
const history = useHistory();
useEffect(() => { useEffect(() => {
if (queryIsEmpty(searchQuery)) {
history.push('/');
return;
}
setLoading({ fetching: true, error: '' }); 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) fetch(fetchURL)
.then((res) => res.json()) .then((res) => res.json())
.then((data) => { .then((data) => {
@ -25,7 +37,7 @@ const SubjectList = ({
console.error(err); console.error(err);
setLoading({ fetching: false, error: err }); setLoading({ fetching: false, error: err });
}); });
}, [setLoading]); }, [setLoading, searchQuery]);
return ( return (
<main className="subjectList"> <main className="subjectList">

View File

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

View File

@ -15,4 +15,12 @@ interface IData {
card_id: number; 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": { "compilerOptions": {
"target": "es5", "target": "es5",
"lib": [ "lib": ["dom", "dom.iterable", "esnext"],
"dom", "allowJs": true,
"dom.iterable", "skipLibCheck": true,
"esnext" "esModuleInterop": true,
], "allowSyntheticDefaultImports": true,
"allowJs": true, "strict": true,
"skipLibCheck": true, "forceConsistentCasingInFileNames": true,
"esModuleInterop": true, "module": "esnext",
"allowSyntheticDefaultImports": true, "moduleResolution": "node",
"strict": true, "resolveJsonModule": true,
"forceConsistentCasingInFileNames": true, "isolatedModules": true,
"module": "esnext", "noEmit": true,
"moduleResolution": "node", "jsx": "react",
"resolveJsonModule": true, "suppressImplicitAnyIndexErrors": true
"isolatedModules": true, },
"noEmit": true, "include": ["src"]
"jsx": "react"
},
"include": [
"src"
]
} }