Globally refactored project structure, added login and upload document forms
32
src/App.tsx
@ -2,13 +2,15 @@ import React, { useEffect, useRef, useState } from 'react';
|
|||||||
import { Switch, Route, useHistory } from 'react-router-dom';
|
import { Switch, Route, useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
import './App.css';
|
import './App.css';
|
||||||
import Header from './Header';
|
import Header from './components/navigation/Header';
|
||||||
import Home from './Home';
|
import Home from './views/Home';
|
||||||
import Navbar from './Navbar';
|
import Navbar from './components/navigation/Navbar';
|
||||||
import { queryIsEmpty } from './Navbar/utils';
|
import { queryIsEmpty } from './components/navigation/Navbar/utils';
|
||||||
import SubjectList from './SubjectList';
|
import SubjectList from './views/SubjectList';
|
||||||
import { ILoadingState, IFilterQuery } from './types';
|
import { ILoadingState, IFilterQuery } from './types';
|
||||||
import NothingFound from './NothingFound';
|
import NothingFound from './views/NothingFound';
|
||||||
|
import UploadForm from './views/Admin/UploadForm';
|
||||||
|
import LogInForm from './views/Admin/LogInForm';
|
||||||
|
|
||||||
const useDidUpdate: typeof useEffect = (func, dependencies) => {
|
const useDidUpdate: typeof useEffect = (func, dependencies) => {
|
||||||
const didMountRef = useRef(false);
|
const didMountRef = useRef(false);
|
||||||
@ -28,6 +30,8 @@ const useDidUpdate: typeof useEffect = (func, dependencies) => {
|
|||||||
const App = () => {
|
const App = () => {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
|
const [token, setToken] = useState<string | null>(localStorage.getItem('token'));
|
||||||
|
|
||||||
const [loading, setLoading] = useState<ILoadingState>({
|
const [loading, setLoading] = useState<ILoadingState>({
|
||||||
fetching: true,
|
fetching: true,
|
||||||
error: ''
|
error: ''
|
||||||
@ -49,7 +53,11 @@ const App = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header query={searchQuery} loading={loading} setSearchQuery={setSearchQuery} />
|
<Header
|
||||||
|
query={searchQuery}
|
||||||
|
loading={loading}
|
||||||
|
setSearchQuery={setSearchQuery}
|
||||||
|
/>
|
||||||
<Navbar query={searchQuery} setSearchQuery={setSearchQuery} />
|
<Navbar query={searchQuery} setSearchQuery={setSearchQuery} />
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path="/">
|
<Route exact path="/">
|
||||||
@ -64,6 +72,16 @@ const App = () => {
|
|||||||
setLoading={setLoading}
|
setLoading={setLoading}
|
||||||
/>
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route path="/u">
|
||||||
|
<UploadForm setLoading={setLoading} token={token} setToken={setToken} />
|
||||||
|
</Route>
|
||||||
|
<Route path="/l">
|
||||||
|
<LogInForm
|
||||||
|
setLoading={setLoading}
|
||||||
|
token={token}
|
||||||
|
setToken={setToken}
|
||||||
|
/>
|
||||||
|
</Route>
|
||||||
<Route path="*">
|
<Route path="*">
|
||||||
<NothingFound setLoading={setLoading} />
|
<NothingFound setLoading={setLoading} />
|
||||||
</Route>
|
</Route>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { IData } from '../types';
|
import { IData } from '../../../types';
|
||||||
|
|
||||||
import './main.css';
|
import './main.css';
|
||||||
|
|
@ -2,7 +2,7 @@ import React, { Dispatch, SetStateAction } from 'react';
|
|||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import { IFilterQuery, ILoadingState } from '../types';
|
import { IFilterQuery, ILoadingState } from '../../../types';
|
||||||
import './main.css';
|
import './main.css';
|
||||||
import Logotype from '../Logotype';
|
import Logotype from '../Logotype';
|
||||||
import { genName } from './utils';
|
import { genName } from './utils';
|
@ -1,4 +1,4 @@
|
|||||||
import { IFilterQuery } from '../types';
|
import { IFilterQuery } from '../../../types';
|
||||||
|
|
||||||
const genName = (
|
const genName = (
|
||||||
searchQuery: IFilterQuery,
|
searchQuery: IFilterQuery,
|
||||||
@ -8,6 +8,15 @@ const genName = (
|
|||||||
if (error) {
|
if (error) {
|
||||||
return error.toString();
|
return error.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (path === '/u') {
|
||||||
|
return 'Upload form';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path === '/l') {
|
||||||
|
return 'login';
|
||||||
|
}
|
||||||
|
|
||||||
if (path === '/list' && searchQuery) {
|
if (path === '/list' && searchQuery) {
|
||||||
let result = '';
|
let result = '';
|
||||||
|
|
@ -2,7 +2,7 @@ import React, { Dispatch, SetStateAction } from 'react';
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { emptyQuery } from '../Navbar/utils';
|
import { emptyQuery } from '../Navbar/utils';
|
||||||
import { IFilterQuery } from '../types';
|
import { IFilterQuery } from '../../../types';
|
||||||
import LogoImage from './logo.png';
|
import LogoImage from './logo.png';
|
||||||
import './main.css';
|
import './main.css';
|
||||||
|
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
@ -1,5 +1,5 @@
|
|||||||
import { Dispatch, RefObject, SetStateAction } from 'react';
|
import { Dispatch, RefObject, SetStateAction } from 'react';
|
||||||
import { IFilterQuery } from '../types';
|
import { IFilterQuery } from '../../../types';
|
||||||
|
|
||||||
const handleFiltersButton = (
|
const handleFiltersButton = (
|
||||||
filtersCollapsed: boolean,
|
filtersCollapsed: boolean,
|
@ -10,8 +10,8 @@ import { motion } from 'framer-motion';
|
|||||||
import './main.css';
|
import './main.css';
|
||||||
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 { IFilterQuery } from '../../../types';
|
||||||
import { useFocus } from '../utils';
|
import { useFocus } from '../../../utils';
|
||||||
import Logotype from '../Logotype';
|
import Logotype from '../Logotype';
|
||||||
import {
|
import {
|
||||||
filtersVariants,
|
filtersVariants,
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
@ -1,5 +1,5 @@
|
|||||||
import { Dispatch, SetStateAction } from 'react';
|
import { Dispatch, SetStateAction } from 'react';
|
||||||
import { IFilterQuery } from '../types';
|
import { IFilterQuery } from '../../../types';
|
||||||
|
|
||||||
const queryIsEmpty = (q: IFilterQuery): boolean => {
|
const queryIsEmpty = (q: IFilterQuery): boolean => {
|
||||||
for (const value of Object.values(q)) {
|
for (const value of Object.values(q)) {
|
25
src/components/uploadForm/Select/index.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
type props = {
|
||||||
|
label: string;
|
||||||
|
name: string;
|
||||||
|
options: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const Select: React.FC<props> = ({ label, name, options }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<label htmlFor={name}>{label}</label>
|
||||||
|
<select name={name} id={name}>
|
||||||
|
<option defaultValue={undefined} value="">
|
||||||
|
-
|
||||||
|
</option>
|
||||||
|
{options.map((val, index) => (
|
||||||
|
<option key={index} value={val}>{val}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Select;
|
0
src/components/uploadForm/Select/main.css
Normal file
25
src/utils.ts
@ -8,4 +8,27 @@ const useFocus = (focusRef: React.RefObject<HTMLInputElement>) => {
|
|||||||
return setFocus;
|
return setFocus;
|
||||||
};
|
};
|
||||||
|
|
||||||
export { useFocus };
|
const handleFormSubmit = (
|
||||||
|
e: React.FormEvent<HTMLFormElement>,
|
||||||
|
uri: string,
|
||||||
|
callBack?: (res: Response) => void,
|
||||||
|
headers?: Headers | string[][] | Record<string, string> | undefined
|
||||||
|
) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const data = new FormData(e.currentTarget);
|
||||||
|
|
||||||
|
const options: RequestInit = {
|
||||||
|
method: 'POST',
|
||||||
|
body: data,
|
||||||
|
headers: headers
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch('https://upml-bank.dmitriy.icu/' + uri, options)
|
||||||
|
.then((res) => {
|
||||||
|
if (!res.ok) throw res.statusText;
|
||||||
|
if (callBack) callBack(res);
|
||||||
|
})
|
||||||
|
.catch((err) => console.log(err));
|
||||||
|
};
|
||||||
|
|
||||||
|
export { useFocus, handleFormSubmit };
|
||||||
|
13
src/views/Admin/LogInForm/handlers.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { Dispatch, SetStateAction } from 'react';
|
||||||
|
|
||||||
|
const handleSuccessfulLogin = (
|
||||||
|
res: Response,
|
||||||
|
setToken: Dispatch<SetStateAction<string | null>>
|
||||||
|
) => {
|
||||||
|
res.json().then(({ token }) => {
|
||||||
|
localStorage.setItem('token', token);
|
||||||
|
setToken(token);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export { handleSuccessfulLogin };
|
40
src/views/Admin/LogInForm/index.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import React, { Dispatch, SetStateAction, useEffect } from 'react';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import { ILoadingState } from '../../../types';
|
||||||
|
import { handleFormSubmit } from '../../../utils';
|
||||||
|
import { handleSuccessfulLogin } from './handlers';
|
||||||
|
|
||||||
|
type props = {
|
||||||
|
setLoading: Dispatch<SetStateAction<ILoadingState>>;
|
||||||
|
token: string | null;
|
||||||
|
setToken: Dispatch<SetStateAction<string | null>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const LogInForm: React.FC<props> = ({ setLoading, token, setToken }) => {
|
||||||
|
const { push: historyPush } = useHistory();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading({ fetching: false, error: '' });
|
||||||
|
if (token) {
|
||||||
|
historyPush('/u');
|
||||||
|
}
|
||||||
|
}, [setLoading, historyPush, token]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={(e) =>
|
||||||
|
handleFormSubmit(e, 'api/login', (res: Response) => handleSuccessfulLogin(res, setToken))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<label htmlFor="">Имя пользователя</label>
|
||||||
|
<input type="text" name="username" />
|
||||||
|
|
||||||
|
<label htmlFor="password">Пароль</label>
|
||||||
|
<input type="password" name="password" />
|
||||||
|
|
||||||
|
<input type="submit" value="Вход" />
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LogInForm;
|
0
src/views/Admin/LogInForm/main.css
Normal file
78
src/views/Admin/UploadForm/index.tsx
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import React, { Dispatch, SetStateAction, useEffect } from 'react';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
|
import Select from '../../../components/uploadForm/Select';
|
||||||
|
import { ILoadingState } from '../../../types';
|
||||||
|
import { handleFormSubmit } from '../../../utils';
|
||||||
|
import selectOptions from './selectOptions.json';
|
||||||
|
|
||||||
|
type props = {
|
||||||
|
setLoading: Dispatch<SetStateAction<ILoadingState>>;
|
||||||
|
token: string | null;
|
||||||
|
setToken: Dispatch<SetStateAction<string | null>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const UploadForm: React.FC<props> = ({ setLoading, token, setToken }) => {
|
||||||
|
const { push: historyPush } = useHistory();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading({ fetching: false, error: '' });
|
||||||
|
if (!token) {
|
||||||
|
historyPush('/l');
|
||||||
|
}
|
||||||
|
}, [setLoading, historyPush, token]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<form
|
||||||
|
onSubmit={(e) =>
|
||||||
|
handleFormSubmit(e, 'api/card/create', undefined, {
|
||||||
|
Authorization: `Token ${localStorage.token}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<label htmlFor="title">Название</label>
|
||||||
|
<input type="text" name="title" />
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label="Преподаватель"
|
||||||
|
name="teacher"
|
||||||
|
options={selectOptions.teacher}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label="Тип задания"
|
||||||
|
name="type_num"
|
||||||
|
options={selectOptions.type_num}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label="Класс"
|
||||||
|
name="class_num"
|
||||||
|
options={selectOptions.class_num}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label="Предмет"
|
||||||
|
name="predmet_type"
|
||||||
|
options={selectOptions.predmet_type}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label htmlFor="image">Файл</label>
|
||||||
|
<input type="file" name="image" id="image" />
|
||||||
|
|
||||||
|
<input type="submit" value="Отправить" />
|
||||||
|
</form>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
setToken(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Выход
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UploadForm;
|
0
src/views/Admin/UploadForm/main.css
Normal file
20
src/views/Admin/UploadForm/selectOptions.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"teacher": [
|
||||||
|
"Попов Д.А",
|
||||||
|
"Ильин А.Б",
|
||||||
|
"Пачин И.М",
|
||||||
|
"Николаева Л.Н",
|
||||||
|
"Ню В.В",
|
||||||
|
"Вишневская Е.А",
|
||||||
|
"Некрасов М.В",
|
||||||
|
"Попова Н.А",
|
||||||
|
"Пачин М.Ф",
|
||||||
|
"Керамов Н.Д",
|
||||||
|
"Новожилова В.И",
|
||||||
|
"Шпехт А.Ю",
|
||||||
|
"Конкина Н.В"
|
||||||
|
],
|
||||||
|
"type_num": ["Семестровки", "Семинары", "Потоковые"],
|
||||||
|
"class_num": ["10", "11"],
|
||||||
|
"predmet_type": ["Математика", "Физика", "Информатика"]
|
||||||
|
}
|
7
src/views/Admin/index.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const Admin: React.FC = () => {
|
||||||
|
return <div></div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Admin;
|
0
src/views/Admin/main.css
Normal file
@ -1,5 +1,5 @@
|
|||||||
import { Dispatch, SetStateAction } from 'react';
|
import { Dispatch, SetStateAction } from 'react';
|
||||||
import { IFilterQuery } from '../types';
|
import { IFilterQuery } from '../../types';
|
||||||
|
|
||||||
const handleShowMore = (
|
const handleShowMore = (
|
||||||
predmet_type: string,
|
predmet_type: string,
|
@ -1,9 +1,9 @@
|
|||||||
import React, { Dispatch, SetStateAction, 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 '../../components/lists/Card';
|
||||||
import NothingFound from '../NothingFound';
|
import NothingFound from '../NothingFound';
|
||||||
import { IData, IFilterQuery, ILoadingState } from '../types';
|
import { IData, IFilterQuery, ILoadingState } from '../../types';
|
||||||
import { handleShowMore } from './handlers';
|
import { handleShowMore } from './handlers';
|
||||||
import './main.css';
|
import './main.css';
|
||||||
|
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
@ -2,7 +2,7 @@ import React, { useEffect, Dispatch, SetStateAction } from 'react';
|
|||||||
|
|
||||||
import './main.css';
|
import './main.css';
|
||||||
import icon from './icon.svg';
|
import icon from './icon.svg';
|
||||||
import { ILoadingState } from '../types';
|
import { ILoadingState } from '../../types';
|
||||||
|
|
||||||
type props = {
|
type props = {
|
||||||
setLoading?: Dispatch<SetStateAction<ILoadingState>>;
|
setLoading?: Dispatch<SetStateAction<ILoadingState>>;
|
@ -1,9 +1,9 @@
|
|||||||
import React, { useEffect, useState, Dispatch, SetStateAction } from 'react';
|
import React, { useEffect, useState, Dispatch, SetStateAction } from 'react';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
import { IData, ILoadingState, IFilterQuery } from '../types';
|
import { IData, ILoadingState, IFilterQuery } from '../../types';
|
||||||
import { queryIsEmpty } from '../Navbar/utils';
|
import { queryIsEmpty } from '../../components/navigation/Navbar/utils';
|
||||||
import Card from '../Card';
|
import Card from '../../components/lists/Card';
|
||||||
import './main.css';
|
import './main.css';
|
||||||
import NothingFound from '../NothingFound';
|
import NothingFound from '../NothingFound';
|
||||||
|
|