From 403c57cdba129ac0dbf00b6c7bceda897c610c7d Mon Sep 17 00:00:00 2001 From: Dm1tr1y147 Date: Fri, 18 Sep 2020 23:57:35 +0500 Subject: [PATCH] Added most of backend interactions and rewrote filers logic --- src/App.tsx | 104 +++++++++++++------- src/Card/index.jsx | 12 --- src/Card/index.tsx | 16 ++++ src/Home/index.tsx | 54 ++++++----- src/Navbar/index.tsx | 194 ++++++++++++++++++++++++++++---------- src/Navbar/utils.ts | 24 +++++ src/SubjectList/index.tsx | 22 ++++- src/index.css | 4 + src/types.ts | 10 +- src/utils.ts | 11 +++ tsconfig.json | 41 ++++---- 11 files changed, 346 insertions(+), 146 deletions(-) delete mode 100644 src/Card/index.jsx create mode 100644 src/Card/index.tsx create mode 100644 src/Navbar/utils.ts create mode 100644 src/utils.ts diff --git a/src/App.tsx b/src/App.tsx index 4792591..c4b2626 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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({ 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({ + fetching: true, + error: '' + }); + + const [searchQuery, setSearchQuery] = useState({ + search: '', + class_num: '', + type_num: '', + predmet_type: '', + teacher: '' + }); + + useDidUpdate(() => { + if (queryIsEmpty(searchQuery)) return; + + history.push('/list'); + }, [searchQuery]); return (
-

{genName(location.pathname, location.search)}

+

{genName(searchQuery, history.location.pathname)}

- + - + - +

404

@@ -64,6 +100,6 @@ function App() {
); -} +}; export default App; diff --git a/src/Card/index.jsx b/src/Card/index.jsx deleted file mode 100644 index 059d68f..0000000 --- a/src/Card/index.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from "react"; - -import './main.css' - -const Card = (props) => ( - -

{props.data.title}

-
{props.data.teacher}
-
-); - -export default Card; diff --git a/src/Card/index.tsx b/src/Card/index.tsx new file mode 100644 index 0000000..9dcdabf --- /dev/null +++ b/src/Card/index.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { IData } from '../types'; + +import './main.css'; + +const Card = ({ data }: { data: IData }) => ( + +

{data.title}

+
{data.teacher}
+
+); + +export default Card; diff --git a/src/Home/index.tsx b/src/Home/index.tsx index f96c2e2..0397e97 100644 --- a/src/Home/index.tsx +++ b/src/Home/index.tsx @@ -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}) => { +const Home = ({ + setLoading, + setSearchQuery +}: { + setLoading: Dispatch>; + setSearchQuery: Dispatch>; +}) => { const [data, setData] = useState([]); 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}) => { }, [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 (
@@ -62,12 +71,13 @@ const Home = ({ setLoading }: {setLoading: Dispatch}) => {
>; + query: IFilterQuery; +}) => { + /* + * Hooks + */ -const Navbar = () => { const [searchCollapsed, setSearchCollapsed] = useState(true); const [filtersCollapsed, setFiltersCollapsed] = useState(true); + const [localFilters, setLocalFilters] = useState>(); + + const searchInput = useRef(null); + + const setInputFocus = useFocus(searchInput); + + const formRef = useRef(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) => { + 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'} >