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 './App.css';
|
||||
import Header from './Header';
|
||||
import Home from './Home';
|
||||
import Navbar from './Navbar';
|
||||
import { queryIsEmpty } from './Navbar/utils';
|
||||
import SubjectList from './SubjectList';
|
||||
import Header from './components/navigation/Header';
|
||||
import Home from './views/Home';
|
||||
import Navbar from './components/navigation/Navbar';
|
||||
import { queryIsEmpty } from './components/navigation/Navbar/utils';
|
||||
import SubjectList from './views/SubjectList';
|
||||
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 didMountRef = useRef(false);
|
||||
@ -28,6 +30,8 @@ const useDidUpdate: typeof useEffect = (func, dependencies) => {
|
||||
const App = () => {
|
||||
const history = useHistory();
|
||||
|
||||
const [token, setToken] = useState<string | null>(localStorage.getItem('token'));
|
||||
|
||||
const [loading, setLoading] = useState<ILoadingState>({
|
||||
fetching: true,
|
||||
error: ''
|
||||
@ -49,7 +53,11 @@ const App = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header query={searchQuery} loading={loading} setSearchQuery={setSearchQuery} />
|
||||
<Header
|
||||
query={searchQuery}
|
||||
loading={loading}
|
||||
setSearchQuery={setSearchQuery}
|
||||
/>
|
||||
<Navbar query={searchQuery} setSearchQuery={setSearchQuery} />
|
||||
<Switch>
|
||||
<Route exact path="/">
|
||||
@ -64,6 +72,16 @@ const App = () => {
|
||||
setLoading={setLoading}
|
||||
/>
|
||||
</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="*">
|
||||
<NothingFound setLoading={setLoading} />
|
||||
</Route>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { IData } from '../types';
|
||||
import { IData } from '../../../types';
|
||||
|
||||
import './main.css';
|
||||
|
@ -2,7 +2,7 @@ import React, { Dispatch, SetStateAction } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { IFilterQuery, ILoadingState } from '../types';
|
||||
import { IFilterQuery, ILoadingState } from '../../../types';
|
||||
import './main.css';
|
||||
import Logotype from '../Logotype';
|
||||
import { genName } from './utils';
|
@ -1,4 +1,4 @@
|
||||
import { IFilterQuery } from '../types';
|
||||
import { IFilterQuery } from '../../../types';
|
||||
|
||||
const genName = (
|
||||
searchQuery: IFilterQuery,
|
||||
@ -8,6 +8,15 @@ const genName = (
|
||||
if (error) {
|
||||
return error.toString();
|
||||
}
|
||||
|
||||
if (path === '/u') {
|
||||
return 'Upload form';
|
||||
}
|
||||
|
||||
if (path === '/l') {
|
||||
return 'login';
|
||||
}
|
||||
|
||||
if (path === '/list' && searchQuery) {
|
||||
let result = '';
|
||||
|
@ -2,7 +2,7 @@ import React, { Dispatch, SetStateAction } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { emptyQuery } from '../Navbar/utils';
|
||||
import { IFilterQuery } from '../types';
|
||||
import { IFilterQuery } from '../../../types';
|
||||
import LogoImage from './logo.png';
|
||||
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 { IFilterQuery } from '../types';
|
||||
import { IFilterQuery } from '../../../types';
|
||||
|
||||
const handleFiltersButton = (
|
||||
filtersCollapsed: boolean,
|
@ -10,8 +10,8 @@ import { motion } from 'framer-motion';
|
||||
import './main.css';
|
||||
import FilterIcon from './filter.svg';
|
||||
import SearchIcon from './search.svg';
|
||||
import { IFilterQuery } from '../types';
|
||||
import { useFocus } from '../utils';
|
||||
import { IFilterQuery } from '../../../types';
|
||||
import { useFocus } from '../../../utils';
|
||||
import Logotype from '../Logotype';
|
||||
import {
|
||||
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 { IFilterQuery } from '../types';
|
||||
import { IFilterQuery } from '../../../types';
|
||||
|
||||
const queryIsEmpty = (q: IFilterQuery): boolean => {
|
||||
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;
|
||||
};
|
||||
|
||||
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 { IFilterQuery } from '../types';
|
||||
import { IFilterQuery } from '../../types';
|
||||
|
||||
const handleShowMore = (
|
||||
predmet_type: string,
|
@ -1,9 +1,9 @@
|
||||
import React, { Dispatch, SetStateAction, useEffect, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import Card from '../Card';
|
||||
import Card from '../../components/lists/Card';
|
||||
import NothingFound from '../NothingFound';
|
||||
import { IData, IFilterQuery, ILoadingState } from '../types';
|
||||
import { IData, IFilterQuery, ILoadingState } from '../../types';
|
||||
import { handleShowMore } from './handlers';
|
||||
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 icon from './icon.svg';
|
||||
import { ILoadingState } from '../types';
|
||||
import { ILoadingState } from '../../types';
|
||||
|
||||
type props = {
|
||||
setLoading?: Dispatch<SetStateAction<ILoadingState>>;
|
@ -1,9 +1,9 @@
|
||||
import React, { useEffect, useState, Dispatch, SetStateAction } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { IData, ILoadingState, IFilterQuery } from '../types';
|
||||
import { queryIsEmpty } from '../Navbar/utils';
|
||||
import Card from '../Card';
|
||||
import { IData, ILoadingState, IFilterQuery } from '../../types';
|
||||
import { queryIsEmpty } from '../../components/navigation/Navbar/utils';
|
||||
import Card from '../../components/lists/Card';
|
||||
import './main.css';
|
||||
import NothingFound from '../NothingFound';
|
||||
|