Compare commits
1 Commits
Author | SHA1 | Date | |
---|---|---|---|
a47178cf13 |
24
.eslintrc.cjs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: { browser: true, es2020: true },
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:react/recommended',
|
||||||
|
'plugin:react/jsx-runtime',
|
||||||
|
'plugin:react-hooks/recommended',
|
||||||
|
],
|
||||||
|
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||||
|
parserOptions: { ecmaVersion: 12, sourceType: 'module' },
|
||||||
|
settings: { react: { version: '18.2' } },
|
||||||
|
plugins: ['react-refresh'],
|
||||||
|
rules: {
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
'indent': 'off',
|
||||||
|
'no-console': 'off',
|
||||||
|
'arrow-body-style': 'off',
|
||||||
|
'implicit-arrow-linebreak': 'off',
|
||||||
|
},
|
||||||
|
}
|
@ -1,3 +1,6 @@
|
|||||||
# PIbd-22_Internet_programming
|
Реализация
|
||||||
|
- регистрация, вход, выход
|
||||||
Интернет-программирование
|
- отправка отзыва
|
||||||
|
- поиск направления
|
||||||
|
- поиск новости по тегу
|
||||||
|
- редактирование новостей администратором
|
32
essense's list.txt
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
user {
|
||||||
|
id,
|
||||||
|
login,
|
||||||
|
FIO,
|
||||||
|
email,
|
||||||
|
birthDate,
|
||||||
|
password,
|
||||||
|
}
|
||||||
|
|
||||||
|
letter {
|
||||||
|
id,
|
||||||
|
email,
|
||||||
|
theme,
|
||||||
|
question
|
||||||
|
}
|
||||||
|
|
||||||
|
news {
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
date,
|
||||||
|
tage,
|
||||||
|
text,
|
||||||
|
image
|
||||||
|
}
|
||||||
|
|
||||||
|
direction {
|
||||||
|
id,
|
||||||
|
code,
|
||||||
|
faculty,
|
||||||
|
name,
|
||||||
|
subjects
|
||||||
|
}
|
14
index.html
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<html lang="ru">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Тольяттинский государственный университет</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root" class="h-100 d-flex flex-column"></div>
|
||||||
|
<script type="module" src="./src/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
15
jsconfig.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"target": "ES2020",
|
||||||
|
"jsx": "react",
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"strictFunctionTypes": true,
|
||||||
|
"sourceMap": true
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"**/node_modules/*"
|
||||||
|
]
|
||||||
|
}
|
5
json-server.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"static": "./node_modules/json-server/public",
|
||||||
|
"port": 8081,
|
||||||
|
"watch": "true"
|
||||||
|
}
|
45
package.json
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"name": "univercity",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"description": "internet-programming 2023",
|
||||||
|
"scripts": {
|
||||||
|
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
||||||
|
"rest": "json-server data.json",
|
||||||
|
"vite": "vite",
|
||||||
|
"dev": "npm-run-all --parallel rest vite",
|
||||||
|
"prod": "npm-run-all lint 'vite build' --parallel rest 'vite preview'"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-free": "6.4.2",
|
||||||
|
"axios": "^1.6.1",
|
||||||
|
"bootstrap": "^5.3.2",
|
||||||
|
"prop-types": "^15.8.1",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-bootstrap": "^2.9.1",
|
||||||
|
"react-bootstrap-icons": "^1.10.3",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-hook-form": "^7.49.2",
|
||||||
|
"react-hot-toast": "^2.4.1",
|
||||||
|
"react-redux": "^9.0.4",
|
||||||
|
"react-router-dom": "^6.18.0",
|
||||||
|
"redux": "^5.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.2.15",
|
||||||
|
"@types/react-dom": "^18.2.15",
|
||||||
|
"@vitejs/plugin-react": "^4.0.3",
|
||||||
|
"eslint": "8.50.0",
|
||||||
|
"eslint-config-airbnb-base": "15.0.0",
|
||||||
|
"eslint-plugin-import": "2.28.1",
|
||||||
|
"eslint-plugin-react": "^7.32.2",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.3",
|
||||||
|
"http-server": "14.1.1",
|
||||||
|
"json-server": "0.17.4",
|
||||||
|
"npm-run-all": "4.1.5",
|
||||||
|
"react-hook-form": "^7.49.2",
|
||||||
|
"vite": "4.4.9"
|
||||||
|
}
|
||||||
|
}
|
BIN
public/favicon.ico
Normal file
After Width: | Height: | Size: 21 KiB |
53
src/App.css
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
html {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0px;
|
||||||
|
padding: 0px;
|
||||||
|
background-color: #e5e5e5;
|
||||||
|
font-family: Arial;
|
||||||
|
/* для закрепления подвала внизу страницы*/
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 800px) {
|
||||||
|
body {
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
background-color: #012362;
|
||||||
|
border: 0px;
|
||||||
|
display: block;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
padding-left: 20px;
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
font-size: 13pt;
|
||||||
|
border: 2px solid;
|
||||||
|
border-left: 10px solid;
|
||||||
|
border-right: 10px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 800px) {
|
||||||
|
table {
|
||||||
|
font-size: 10pt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 500px) {
|
||||||
|
table {
|
||||||
|
font-size: 8pt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
thead {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
26
src/App.jsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import './App.css';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Content from './components/content/Content';
|
||||||
|
import Header from './components/header/Header.jsx';
|
||||||
|
import Navigation from './components/navigation/Navigation.jsx';
|
||||||
|
import Footer from './components/footer/Footer.jsx';
|
||||||
|
import { UserProvider } from './providers/UserProvider';
|
||||||
|
|
||||||
|
const App = ({ routes }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<UserProvider>
|
||||||
|
<Header />
|
||||||
|
<Navigation routes={routes}></Navigation>
|
||||||
|
<Content />
|
||||||
|
<Footer />
|
||||||
|
</UserProvider>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
App.propTypes = {
|
||||||
|
routes: PropTypes.array,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default App;
|
BIN
src/assets/logo.png
Normal file
After Width: | Height: | Size: 6.8 KiB |
40
src/components/api/ApiClient.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
|
export class HttpError extends Error {
|
||||||
|
constructor(message = '') {
|
||||||
|
super(message);
|
||||||
|
this.name = 'HttpError';
|
||||||
|
Object.setPrototypeOf(this, new.target.prototype);
|
||||||
|
toast.error(message, { id: 'HttpError' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function responseHandler(response) {
|
||||||
|
if (response.status === 200 || response.status === 201) {
|
||||||
|
const data = response?.data;
|
||||||
|
if (!data) {
|
||||||
|
throw new HttpError('API Error. No data!');
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
throw new HttpError(`API Error! Invalid status code ${response.status}!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function responseErrorHandler(error) {
|
||||||
|
if (error === null) {
|
||||||
|
throw new Error('Unrecoverable error!! Error is null!');
|
||||||
|
}
|
||||||
|
toast.error(error.message, { id: 'AxiosError' });
|
||||||
|
return Promise.reject(error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ApiClient = axios.create({
|
||||||
|
baseURL: 'http://localhost:8081/',
|
||||||
|
timeout: '5000',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
ApiClient.interceptors.response.use(responseHandler, responseErrorHandler);
|
29
src/components/api/ApiDirection.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { ApiClient } from './ApiClient';
|
||||||
|
|
||||||
|
class ApiDirection {
|
||||||
|
constructor(url) {
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAll(expand) {
|
||||||
|
return ApiClient.get(`${this.url}${expand || ''}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(id, expand) {
|
||||||
|
return ApiClient.get(`${this.url}/${id}${expand || ''}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(body) {
|
||||||
|
return ApiClient.post(this.url, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id, body) {
|
||||||
|
return ApiClient.put(`${this.url}/${id}`, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id) {
|
||||||
|
return ApiClient.delete(`${this.url}/${id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ApiDirection;
|
29
src/components/api/ApiNews.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { ApiClient } from './ApiClient';
|
||||||
|
|
||||||
|
class ApiNews {
|
||||||
|
constructor(url) {
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAll(expand) {
|
||||||
|
return ApiClient.get(`${this.url}${expand || ''}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(id, expand) {
|
||||||
|
return ApiClient.get(`${this.url}/${id}${expand || ''}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(body) {
|
||||||
|
return ApiClient.post(this.url, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id, body) {
|
||||||
|
return ApiClient.put(`${this.url}/${id}`, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id) {
|
||||||
|
return ApiClient.delete(`${this.url}/${id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ApiNews;
|
41
src/components/api/ApiService.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { ApiClient } from './ApiClient';
|
||||||
|
|
||||||
|
class ApiService {
|
||||||
|
constructor(url) {
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAll(expand) {
|
||||||
|
return ApiClient.get(`${this.url}${expand || ''}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(id, expand) {
|
||||||
|
return ApiClient.get(`${this.url}/${id}${expand || ''}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(body) {
|
||||||
|
return ApiClient.post(this.url, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getByLogin(login) {
|
||||||
|
return ApiClient.get(`${this.url}?login=${login}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getByEmail(email) {
|
||||||
|
return ApiClient.get(`${this.url}?email=${email}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllForUser(userId) {
|
||||||
|
return ApiClient.get(`${this.url}?userId=${userId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id, body) {
|
||||||
|
return ApiClient.put(`${this.url}/${id}`, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id) {
|
||||||
|
return ApiClient.delete(`${this.url}/${id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ApiService;
|
34
src/components/api/contactApiService.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { ApiClient } from './ApiClient';
|
||||||
|
|
||||||
|
// id,
|
||||||
|
// email,
|
||||||
|
// theme,
|
||||||
|
// question
|
||||||
|
|
||||||
|
class contactApiService {
|
||||||
|
constructor(url) {
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAll(expand) {
|
||||||
|
return ApiClient.get(`${this.url}${expand || ''}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(id, expand) {
|
||||||
|
return ApiClient.get(`${this.url}/${id}${expand || ''}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(body) {
|
||||||
|
return ApiClient.post(this.url, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id, body) {
|
||||||
|
return ApiClient.put(`${this.url}/${id}`, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id) {
|
||||||
|
return ApiClient.delete(`${this.url}/${id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default contactApiService;
|
37
src/components/content/Content.css
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
#content {
|
||||||
|
width: 80%;
|
||||||
|
margin: 1.5% 10% 1.5% 10%;
|
||||||
|
padding: 30px;
|
||||||
|
padding-left: 5%;
|
||||||
|
padding-right: 5%;
|
||||||
|
background-color: white;
|
||||||
|
color: #012362;
|
||||||
|
font-size: 13pt;
|
||||||
|
text-align: justify;
|
||||||
|
line-height: 2em;
|
||||||
|
/* для закрепления подвала внизу страницы*/
|
||||||
|
flex: 1 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 800px) {
|
||||||
|
#content {
|
||||||
|
width: 100%;
|
||||||
|
margin: 1.5% 0% 1.5% 0%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* центр для изображений*/
|
||||||
|
#content img {
|
||||||
|
display: block;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#content a{
|
||||||
|
color: #06c;
|
||||||
|
}
|
||||||
|
|
||||||
|
#content a:hover{
|
||||||
|
color: #f07c41;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
12
src/components/content/Content.jsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import './Content.css'
|
||||||
|
import { Outlet } from 'react-router-dom';
|
||||||
|
|
||||||
|
const Content = () => {
|
||||||
|
return (
|
||||||
|
<div id="content">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Content;
|
24
src/components/footer/Footer.css
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
#footer {
|
||||||
|
width: 100%;
|
||||||
|
background-color: #012362;
|
||||||
|
text-align: center;
|
||||||
|
font-family: Arial;
|
||||||
|
font-size: 15px; /* в header'e 20px*/
|
||||||
|
color: white;
|
||||||
|
line-height: 1.5em;
|
||||||
|
padding: 14px 0px 14px 0px;
|
||||||
|
/* для закрепления подвала внизу страницы*/
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#footer ul {
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#footer li {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
#footer li:not(:last-of-type) {
|
||||||
|
margin-right: 3%;
|
||||||
|
}
|
18
src/components/footer/Footer.jsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import './Footer.css';
|
||||||
|
|
||||||
|
const Footer = () => {
|
||||||
|
const year = new Date().getFullYear();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="footer">
|
||||||
|
<ul>
|
||||||
|
<li>Адреса и контакты:</li>
|
||||||
|
<li>432027, г. Тольятти, ул. Ларина, д.32</li>
|
||||||
|
<li>+7 (8332) 43-06-55</li>
|
||||||
|
</ul>
|
||||||
|
dex_moth, {year}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Footer;
|
29
src/components/header/Header.css
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
#header {
|
||||||
|
width: 100%;
|
||||||
|
background-color: #012362;
|
||||||
|
text-align: center;
|
||||||
|
font-family: Arial;
|
||||||
|
font-size: 20px;
|
||||||
|
color: white;
|
||||||
|
padding: 10px 0px 10px 0px;
|
||||||
|
/* для закрепления подвала внизу страницы*/
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#header a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#header a:hover {
|
||||||
|
text-shadow: 1px 1px 8px #06c;
|
||||||
|
}
|
||||||
|
|
||||||
|
#logo {
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#logo:hover {
|
||||||
|
filter: drop-shadow(0px 2px 16px #06c);
|
||||||
|
}
|
14
src/components/header/Header.jsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import './Header.css'
|
||||||
|
import universityLogo from '../../assets/logo.png';
|
||||||
|
|
||||||
|
const Header = () => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="header">
|
||||||
|
<a href="/"><img id="logo" src={universityLogo} height="50px"/></a>
|
||||||
|
<a href="/">Тольяттинский государственный политехнический университет</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Header;
|
3
src/components/modal/Modal.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.modal-title {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
34
src/components/modal/ModalConfirm.jsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Button, Modal } from 'react-bootstrap';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
import './Modal.css';
|
||||||
|
|
||||||
|
const ModalConfirm = ({ show, title, message, onConfirm, onClose, }) => (
|
||||||
|
createPortal(
|
||||||
|
<Modal show={show} backdrop='static' onHide={() => onClose()}>
|
||||||
|
<Modal.Header className='pt-2 pb-2 ps-3 pe-3' closeButton>
|
||||||
|
<Modal.Title>{title}</Modal.Title>
|
||||||
|
</Modal.Header>
|
||||||
|
|
||||||
|
<Modal.Body>
|
||||||
|
{message}
|
||||||
|
</Modal.Body>
|
||||||
|
|
||||||
|
<Modal.Footer className='m-0 pt-2 pb-2 ps-3 pe-3 row justify-content-center'>
|
||||||
|
<Button className='col-5 m-0 me-2' onClick={() => onClose()}>Нет</Button>
|
||||||
|
<Button className='col-5 m-0 ms-2' onClick={() => onConfirm()}>Да</Button>
|
||||||
|
</Modal.Footer>
|
||||||
|
</Modal>,
|
||||||
|
document.body,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
ModalConfirm.propTypes = {
|
||||||
|
show: PropTypes.bool,
|
||||||
|
title: PropTypes.string,
|
||||||
|
message: PropTypes.string,
|
||||||
|
onConfirm: PropTypes.func,
|
||||||
|
onClose: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModalConfirm;
|
40
src/components/modal/ModalForm.jsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Button, Form, Modal } from 'react-bootstrap';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
import './Modal.css';
|
||||||
|
|
||||||
|
const ModalForm = ({ show, title, validated, onSubmit, onClose, children, }) => (
|
||||||
|
createPortal(
|
||||||
|
<Modal show={show} backdrop='static' onHide={() => onClose()}>
|
||||||
|
<Modal.Header className='pt-2 pb-2 ps-3 pe-3' closeButton>
|
||||||
|
<Modal.Title>{title}</Modal.Title>
|
||||||
|
</Modal.Header>
|
||||||
|
<Form className='m-0' noValidate validated={validated} onSubmit={onSubmit}>
|
||||||
|
<Modal.Body>
|
||||||
|
{children}
|
||||||
|
</Modal.Body>
|
||||||
|
|
||||||
|
<Modal.Footer className='m-0 pt-2 pb-2 ps-3 pe-3 row justify-content-center'>
|
||||||
|
<Button className='col-5 m-0 me-2'
|
||||||
|
onClick={() => onClose()}>
|
||||||
|
Отмена
|
||||||
|
</Button>
|
||||||
|
<Button className='col-5 m-0 ms-2' type='submit'>
|
||||||
|
Сохранить
|
||||||
|
</Button>
|
||||||
|
</Modal.Footer>
|
||||||
|
</Form>
|
||||||
|
</Modal>,
|
||||||
|
document.body,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
ModalForm.propTypes = {
|
||||||
|
show: PropTypes.bool,
|
||||||
|
title: PropTypes.string,
|
||||||
|
validated: PropTypes.bool,
|
||||||
|
onSubmit: PropTypes.func,
|
||||||
|
onClose: PropTypes.func,
|
||||||
|
children: PropTypes.node,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModalForm;
|
21
src/components/modal/ModalHook.jsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
const useModal = () => {
|
||||||
|
const [showModal, setShowModal] = useState(false);
|
||||||
|
|
||||||
|
const showModalDialog = () => {
|
||||||
|
setShowModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const hideModalDialog = () => {
|
||||||
|
setShowModal(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
isModalShow: showModal,
|
||||||
|
showModal: showModalDialog,
|
||||||
|
hideModal: hideModalDialog,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useModal;
|
53
src/components/navigation/Navigation.css
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
.navbar {
|
||||||
|
width: 100%;
|
||||||
|
background-color: white;
|
||||||
|
color: #012362;
|
||||||
|
font-family: Arial;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand, .nav-item {
|
||||||
|
margin-left: 5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 800px) {
|
||||||
|
.navbar-brand, .nav-item {
|
||||||
|
margin-left: 2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1280px) {
|
||||||
|
.navbar-brand, .nav-item {
|
||||||
|
margin-left: 6em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width: 1600px) {
|
||||||
|
.navbar-brand, .nav-item {
|
||||||
|
margin-left: 8em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width: 1920px) {
|
||||||
|
.navbar-brand, .nav-item {
|
||||||
|
margin-left: 12em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width: 2100px) {
|
||||||
|
.navbar-brand, .nav-item {
|
||||||
|
margin-left: 17em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand, .nav-link {
|
||||||
|
font-size: 14pt;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #012362;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand:hover, .nav-link:hover {
|
||||||
|
color: #f07c41;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand:focus, .navbar-nav .nav-link.active, .navbar-nav .nav-link.show {
|
||||||
|
color: #06c;
|
||||||
|
}
|
68
src/components/navigation/Navigation.jsx
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useContext } from 'react';
|
||||||
|
import { UserContext } from '../../providers/UserProvider';
|
||||||
|
import useUser from '../../providers/hooks/UserHook';
|
||||||
|
import { Container, Nav, Navbar } from 'react-bootstrap';
|
||||||
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
|
import { Person } from 'react-bootstrap-icons';
|
||||||
|
import './Navigation.css'
|
||||||
|
|
||||||
|
const Navigation = ({ routes }) => {
|
||||||
|
|
||||||
|
const location = useLocation();
|
||||||
|
const indexPageLink = routes.filter((route) => route.index === false).shift();
|
||||||
|
const pages = routes.filter((route) => Object.prototype.hasOwnProperty.call(route, 'title'));
|
||||||
|
|
||||||
|
// зашёл ли пользователь
|
||||||
|
const { user } = useContext(UserContext);
|
||||||
|
// админ?
|
||||||
|
const isAdmin = user && user.login === 'admin';
|
||||||
|
// функция выхода из профиля
|
||||||
|
const { userLogout } = useUser();
|
||||||
|
|
||||||
|
console.log(user);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Navbar expand="md" bg="purple">
|
||||||
|
<Container>
|
||||||
|
<Navbar.Brand as={Link} to={indexPageLink?.path ?? '/news'}>
|
||||||
|
Новости
|
||||||
|
</Navbar.Brand>
|
||||||
|
<Navbar.Toggle aria-controls='main-navbar' />
|
||||||
|
<Navbar.Collapse id='main-navbar'>
|
||||||
|
<Nav className='me-auto link' activeKey={location.pathname}>
|
||||||
|
{pages.map((page) => (
|
||||||
|
<Nav.Link as={Link} key={page.path} eventKey={page.path} to={page.path ?? '/'}>
|
||||||
|
{page.title}
|
||||||
|
</Nav.Link>
|
||||||
|
))}
|
||||||
|
</Nav>
|
||||||
|
{user ? (
|
||||||
|
<Nav className='me-auto link'>
|
||||||
|
<Nav.Link as={Link} key="/profile" to='/profile'>
|
||||||
|
<Person className='me-2'/>{user.login}
|
||||||
|
</Nav.Link>
|
||||||
|
{isAdmin && (
|
||||||
|
<Nav.Link as={Link} key='/adminnews' to='/adminnews'>Админка</Nav.Link>
|
||||||
|
)}
|
||||||
|
<Nav.Link as={Link} onClick={userLogout} to='/'>Выйти</Nav.Link>
|
||||||
|
</Nav>
|
||||||
|
) :
|
||||||
|
(
|
||||||
|
<Nav className='me-auto link'>
|
||||||
|
<Nav.Link as={Link} key='/login' eventKey='/login' to='/login'>Войти</Nav.Link>
|
||||||
|
<Nav.Link as={Link} key='/registration' eventKey='/registration' to='/registration'>Зарегистрироваться</Nav.Link>
|
||||||
|
</Nav>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Navbar.Collapse>
|
||||||
|
</Container>
|
||||||
|
</Navbar>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Navigation.propTypes = {
|
||||||
|
routes: PropTypes.array,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Navigation;
|
44
src/components/navigation/components/NavLogic.jsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
// import { useContext } from 'react';
|
||||||
|
// import AdminNav from './components/AdminNav.jsx';
|
||||||
|
// import ExitNav from './components/ExitNav.jsx';
|
||||||
|
// import LoginNav from './components/LoginNav.jsx';
|
||||||
|
// import ProfileNav from './components/ProfileNav.jsx';
|
||||||
|
// import RegistrationNav from './components/RegistrationNav.jsx';
|
||||||
|
// import ContactsNav from './components/Contacts.jsx';
|
||||||
|
// import UniversityNav from './components/UniversityNav.jsx';
|
||||||
|
// import AbiturientNav from './components/AbiturientNav.jsx';
|
||||||
|
|
||||||
|
// const NavLogic= () => {
|
||||||
|
// const user = useContext(UserContext);
|
||||||
|
// console.log("user=" + user);
|
||||||
|
|
||||||
|
// if (user != null) {
|
||||||
|
// return (
|
||||||
|
// <>
|
||||||
|
// <UniversityNav/>
|
||||||
|
// <AbiturientNav/>
|
||||||
|
// <ContactsNav/>
|
||||||
|
// <RegistrationNav/>
|
||||||
|
// <LoginNav/>
|
||||||
|
// <RegistrationNav/>
|
||||||
|
// <ProfileNav/>
|
||||||
|
// <AdminNav/>
|
||||||
|
// <ExitNav/>
|
||||||
|
// </>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// else {
|
||||||
|
// return (
|
||||||
|
// <>
|
||||||
|
// <UniversityNav/>
|
||||||
|
// <AbiturientNav/>
|
||||||
|
// <ContactsNav/>
|
||||||
|
// <RegistrationNav/>
|
||||||
|
// <LoginNav/>
|
||||||
|
// <RegistrationNav/>
|
||||||
|
// </>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// export default NavLogic;
|
@ -0,0 +1,12 @@
|
|||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { Nav } from 'react-bootstrap';
|
||||||
|
|
||||||
|
const AbiturientNav = () => {
|
||||||
|
<>
|
||||||
|
<Nav.Link as={Link} to = {'/abiturient' ?? '/'}>
|
||||||
|
Поступающему
|
||||||
|
</Nav.Link>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AbiturientNav;
|
18
src/components/navigation/components/components/AdminNav.jsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { Nav } from 'react-bootstrap';
|
||||||
|
|
||||||
|
// {
|
||||||
|
// pages.map((page) =>
|
||||||
|
// <Nav.Link as={Link} key={page.path} eventKey={page.path} to={page.path ?? '/'}>
|
||||||
|
// {page.title}
|
||||||
|
// </Nav.Link>)
|
||||||
|
|
||||||
|
const AdminNav = () => {
|
||||||
|
<>
|
||||||
|
<Nav.Link as={Link} to = {'/admin' ?? '/'}>
|
||||||
|
Страница администратора
|
||||||
|
</Nav.Link>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AdminNav;
|
12
src/components/navigation/components/components/Contacts.jsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { Nav } from 'react-bootstrap';
|
||||||
|
|
||||||
|
const ContactsNav = () => {
|
||||||
|
<>
|
||||||
|
<Nav.Link as={Link} to = {'/contacts' ?? '/'}>
|
||||||
|
Контакты
|
||||||
|
</Nav.Link>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ContactsNav;
|
12
src/components/navigation/components/components/ExitNav.jsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { Nav } from 'react-bootstrap';
|
||||||
|
|
||||||
|
const ExitNav = () => {
|
||||||
|
<>
|
||||||
|
<Nav.Link as={Link} to = {'/'}>
|
||||||
|
Выйти
|
||||||
|
</Nav.Link>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ExitNav;
|
12
src/components/navigation/components/components/LoginNav.jsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { Nav } from 'react-bootstrap';
|
||||||
|
|
||||||
|
const LoginNav = () => {
|
||||||
|
<>
|
||||||
|
<Nav.Link as={Link} to = {'/login' ?? '/'}>
|
||||||
|
Войти
|
||||||
|
</Nav.Link>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoginNav;
|
12
src/components/navigation/components/components/NewsNav.jsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { Nav } from 'react-bootstrap';
|
||||||
|
|
||||||
|
const NewsNav = () => {
|
||||||
|
<>
|
||||||
|
<Nav.Link as={Link} to = {'/news' ?? '/'}>
|
||||||
|
Новости
|
||||||
|
</Nav.Link>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NewsNav;
|
@ -0,0 +1,12 @@
|
|||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { Nav } from 'react-bootstrap';
|
||||||
|
|
||||||
|
const ProfileNav = () => {
|
||||||
|
<>
|
||||||
|
<Nav.Link as={Link} to = {'/profile' ?? '/'}>
|
||||||
|
Профиль
|
||||||
|
</Nav.Link>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProfileNav;
|
@ -0,0 +1,12 @@
|
|||||||
|
import { Nav } from 'react-bootstrap';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
const RegistrationNav = () => {
|
||||||
|
<>
|
||||||
|
<Nav.Link as={Link} to = {'/registration' ?? '/'}>
|
||||||
|
Зарегистрироваться
|
||||||
|
</Nav.Link>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RegistrationNav;
|
@ -0,0 +1,12 @@
|
|||||||
|
import { Nav } from 'react-bootstrap';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
const UniversityNav = () => {
|
||||||
|
<>
|
||||||
|
<Nav.Link as={Link} to = {'/university' ?? '/'}>
|
||||||
|
Об университете
|
||||||
|
</Nav.Link>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UniversityNav;
|
5
src/components/services/DirectionApiService.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import ApiDirection from "../api/ApiDirection";
|
||||||
|
|
||||||
|
const DirectionApiService = new ApiDirection('directions');
|
||||||
|
|
||||||
|
export default DirectionApiService;
|
5
src/components/services/NewsApiService.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import ApiNews from '../api/ApiNews';
|
||||||
|
|
||||||
|
const NewsApiService = new ApiNews('news');
|
||||||
|
|
||||||
|
export default NewsApiService;
|
5
src/components/services/UsersApiService.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import ApiService from '../api/ApiService';
|
||||||
|
|
||||||
|
const UsersApiService = new ApiService('users');
|
||||||
|
|
||||||
|
export default UsersApiService;
|
15
src/components/utils/Base64.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
const getBase64 = async (file) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
reader.onloadend = () => {
|
||||||
|
const fileContent = reader.result;
|
||||||
|
resolve(fileContent);
|
||||||
|
};
|
||||||
|
reader.onerror = () => {
|
||||||
|
reject(new Error('Oops, something went wrong with the file reader.'));
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getBase64;
|
0
src/index.css
Normal file
84
src/main.jsx
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import { RouterProvider, createBrowserRouter } from 'react-router-dom';
|
||||||
|
import App from './App.jsx';
|
||||||
|
import './index.css';
|
||||||
|
import Abiturient from './pages/Abiturient/Abiturient.jsx';
|
||||||
|
import Admin from './pages/Admin/Admin.jsx';
|
||||||
|
import AdminNews from './pages/AdminNews/AdminNews.jsx';
|
||||||
|
import Contacts from './pages/Contacts/Contacts.jsx';
|
||||||
|
import ErrorPage from './pages/Errorpage/ErrorPage.jsx';
|
||||||
|
import Login from './pages/Login/Login.jsx';
|
||||||
|
import Main from './pages/Main/Main.jsx';
|
||||||
|
import News from './pages/News/News.jsx';
|
||||||
|
import Profile from './pages/Profile/Profile.jsx';
|
||||||
|
import Registration from './pages/Registration/Registration.jsx';
|
||||||
|
import RegistrationSuccess from './pages/RegistrationSuccess/RegistrationSuccess.jsx';
|
||||||
|
import University from './pages/University/University.jsx';
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/university',
|
||||||
|
element: <University />,
|
||||||
|
title: 'Об университете',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/abiturient',
|
||||||
|
element: <Abiturient />,
|
||||||
|
title: 'Поступающему',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/admin',
|
||||||
|
element: <Admin />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/adminnews',
|
||||||
|
element: <AdminNews />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/registration',
|
||||||
|
element: <Registration />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/contacts',
|
||||||
|
element: <Contacts />,
|
||||||
|
title: 'Контакты',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/news',
|
||||||
|
element: <News />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
index: true,
|
||||||
|
path: '/',
|
||||||
|
element: <Main />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
element: <Login />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/profile',
|
||||||
|
element: <Profile />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/registration_success',
|
||||||
|
element: <RegistrationSuccess />
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const router = createBrowserRouter([
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
element: <App routes={routes} />,
|
||||||
|
children: routes,
|
||||||
|
errorElement: <ErrorPage />,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
</React.StrictMode>,
|
||||||
|
);
|
63
src/pages/Abiturient/Abiturient.jsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import SearchHook from './hooks/SearchHook';
|
||||||
|
import { Form, Table, Container, Row, Col, InputGroup } from 'react-bootstrap';
|
||||||
|
|
||||||
|
const Abiturient = () => {
|
||||||
|
|
||||||
|
const { searchTerm, searchName, searchFaculty, handleChange, handleChange2, handleChange3, searchResults } = SearchHook();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="text-center">
|
||||||
|
<h3>Список направлений</h3>
|
||||||
|
</div>
|
||||||
|
<Form id="directions" >
|
||||||
|
<Container className='mb-4'>
|
||||||
|
<Row sm={2} className="justify-content-md-center">
|
||||||
|
<Col xl={3} className='mt-2'>
|
||||||
|
<InputGroup>
|
||||||
|
<InputGroup.Text>Факультет:</InputGroup.Text>
|
||||||
|
<Form.Control id="faculty" placeholder="ФИСТ" value={searchFaculty} onChange={handleChange3}/>
|
||||||
|
</InputGroup>
|
||||||
|
</Col>
|
||||||
|
<Col md={6} xl={4} className='mt-2'>
|
||||||
|
<InputGroup>
|
||||||
|
<InputGroup.Text>Направление:</InputGroup.Text>
|
||||||
|
<Form.Control id="direction" placeholder="Программная инженерия" value={searchName} onChange={handleChange2}/>
|
||||||
|
</InputGroup>
|
||||||
|
</Col>
|
||||||
|
<Col md={6} xl={5} className='mt-2'>
|
||||||
|
<InputGroup>
|
||||||
|
<InputGroup.Text>Предметы:</InputGroup.Text>
|
||||||
|
<Form.Control id="subjects" placeholder="Информатика" value={searchTerm} onChange={handleChange}/>
|
||||||
|
</InputGroup>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Container>
|
||||||
|
</Form>
|
||||||
|
<Table className="mt-2" bordered hover>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Код</th>
|
||||||
|
<th scope="col">Факультет</th>
|
||||||
|
<th scope="col">Наименование направления</th>
|
||||||
|
<th scope="col" className='w-50'>Требуемые баллы ЕГЭ</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{
|
||||||
|
searchResults.map((line) =>
|
||||||
|
<tr key={line.id}>
|
||||||
|
<td scope="col">{line.code}</td>
|
||||||
|
<td scope="col">{line.faculty}</td>
|
||||||
|
<td scope="col">{line.name}</td>
|
||||||
|
<td scope="col">{line.subjects}</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Abiturient;
|
56
src/pages/Abiturient/hooks/SearchHook.jsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import DirectionApiService from '../../../components/services/DirectionApiService';
|
||||||
|
|
||||||
|
const useDirection = () => {
|
||||||
|
const [linesRefresh, setLinesRefresh] = useState(false);
|
||||||
|
const [lines, setLines] = useState([]);
|
||||||
|
const handleLinesChange = () => setLinesRefresh(!linesRefresh);
|
||||||
|
|
||||||
|
const getLines = async () => {
|
||||||
|
const data = await DirectionApiService.getAll();
|
||||||
|
setLines(data ?? []);
|
||||||
|
};
|
||||||
|
|
||||||
|
// для поиска
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [searchName, setSearchName] = useState("");
|
||||||
|
const [searchFaculty, setSearchFaculty] = useState("");
|
||||||
|
const [searchResults, setSearchResults] = useState([]);
|
||||||
|
|
||||||
|
const handleChange = ( event ) => {
|
||||||
|
setSearchTerm(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange2 = ( event ) => {
|
||||||
|
setSearchName(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange3 = ( event ) => {
|
||||||
|
setSearchFaculty(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getLines();
|
||||||
|
const results = lines.filter(line =>
|
||||||
|
line.subjects.toLowerCase().includes(searchTerm)
|
||||||
|
);
|
||||||
|
const results2 = results.filter(line =>
|
||||||
|
line.name.toLowerCase().includes(searchName)
|
||||||
|
);
|
||||||
|
const results3 = results2.filter(line =>
|
||||||
|
line.faculty.toLowerCase().includes(searchFaculty)
|
||||||
|
);
|
||||||
|
|
||||||
|
setSearchResults(results3);
|
||||||
|
}, [searchTerm, searchName, searchFaculty, linesRefresh]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
searchResults,
|
||||||
|
handleLinesChange,
|
||||||
|
handleChange,
|
||||||
|
handleChange2,
|
||||||
|
handleChange3
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useDirection;
|
28
src/pages/Admin/Admin.jsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { Button, Container } from 'react-bootstrap';
|
||||||
|
import { Navigate } from 'react-router-dom';
|
||||||
|
import { useContext } from 'react';
|
||||||
|
import { UserContext } from '../../providers/UserProvider';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
const Admin = () => {
|
||||||
|
const { user } = useContext(UserContext);
|
||||||
|
const isAdmin = user && user.login === 'admin';
|
||||||
|
|
||||||
|
if (!isAdmin) {
|
||||||
|
return <Navigate to='/' replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="text-center">
|
||||||
|
<h3>Кабинет администратора</h3>
|
||||||
|
</div>
|
||||||
|
<br/>
|
||||||
|
<Container>
|
||||||
|
<Link to='/adminnews'><Button>Таблица новостей</Button></Link>
|
||||||
|
</Container>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Admin;
|
25
src/pages/AdminNews/AdminNews.jsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { Navigate } from 'react-router-dom';
|
||||||
|
import { useContext } from 'react';
|
||||||
|
import { UserContext } from '../../providers/UserProvider';
|
||||||
|
import News from './components/News';
|
||||||
|
|
||||||
|
const AdminNews = () => {
|
||||||
|
const { user } = useContext(UserContext);
|
||||||
|
const isAdmin = user && user.login === 'admin';
|
||||||
|
|
||||||
|
if (!isAdmin) {
|
||||||
|
return <Navigate to='/' replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="text-center">
|
||||||
|
<h3>Кабинет администратора</h3>
|
||||||
|
<h4>Таблица новостей</h4>
|
||||||
|
</div>
|
||||||
|
<News/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AdminNews;
|
38
src/pages/AdminNews/components/News.jsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import useLines from "../hooks/useLines";
|
||||||
|
import NewsTable from "./NewsTable";
|
||||||
|
import NewsTableRow from "./NewsTableRow";
|
||||||
|
import ModalConfirm from '../../../components/modal/ModalConfirm';
|
||||||
|
import ModalForm from '../../../components/modal/ModalForm';
|
||||||
|
import NewsForm from "./NewsForm";
|
||||||
|
import NewsDelete from '../hooks/NewsDeleteHook';
|
||||||
|
import NewsFormModal from '../hooks/NewsFormHook';
|
||||||
|
import { Button } from "react-bootstrap";
|
||||||
|
|
||||||
|
const News = () =>
|
||||||
|
{
|
||||||
|
const { lines, handleLinesChange } = useLines();
|
||||||
|
const { isDeleteModalShow, showDeleteModal, handleDeleteConfirm, handleDeleteCancel, } = NewsDelete(handleLinesChange);
|
||||||
|
const {isFormModalShow, isFormValidated, showFormModal, currentItem, handleItemChange, handleFormSubmit, handleFormClose, } = NewsFormModal(handleLinesChange);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button onClick={() => showFormModal(0)}>Добавить новость</Button>
|
||||||
|
<NewsTable>
|
||||||
|
{
|
||||||
|
lines.map((line, index) =>
|
||||||
|
<NewsTableRow key={line.id}
|
||||||
|
index={index} post={line}
|
||||||
|
onDelete={() => showDeleteModal(line.id)}
|
||||||
|
onEditInPage={() => showFormModal(line.id)}
|
||||||
|
/>)
|
||||||
|
}
|
||||||
|
</NewsTable>
|
||||||
|
<ModalConfirm show={isDeleteModalShow} onConfirm={handleDeleteConfirm} onClose={handleDeleteCancel} title='Удаление' message='Удалить элемент?' />
|
||||||
|
<ModalForm show={isFormModalShow} validated={isFormValidated} onSubmit={handleFormSubmit} onClose={handleFormClose} title='Редактирование'>
|
||||||
|
<NewsForm item={currentItem} handleChange={handleItemChange} />
|
||||||
|
</ModalForm>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default News;
|
46
src/pages/AdminNews/components/NewsForm.jsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Form, InputGroup } from 'react-bootstrap';
|
||||||
|
|
||||||
|
const NewsForm = ({item, handleChange}) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className='text-center'>
|
||||||
|
<img src={item.image || "https://via.placeholder.com/200"} className="rounded" alt="placeholder" style={{ width: "100%", height: "330px" }}/>
|
||||||
|
</div>
|
||||||
|
<br/>
|
||||||
|
<Form.Group controlId="title">
|
||||||
|
<Form.Label>Заголовок:</Form.Label>
|
||||||
|
<Form.Control name="title" value={item.title} type="text" placeholder="Заголовок" required onChange={handleChange} />
|
||||||
|
</Form.Group>
|
||||||
|
<br/>
|
||||||
|
<Form.Group controlId="date">
|
||||||
|
<Form.Label>Дата:</Form.Label>
|
||||||
|
<Form.Control name="date" value={item.date} type="date" placeholder="01.01.2024" required onChange={handleChange}/>
|
||||||
|
</Form.Group>
|
||||||
|
<br/>
|
||||||
|
<Form.Group controlId="text">
|
||||||
|
<Form.Label>Текст новости:</Form.Label>
|
||||||
|
<Form.Control name="text" value={item.text} type="text" as="textarea" placeholder="2-3 предложения" rows={4} required onChange={handleChange}/>
|
||||||
|
</Form.Group>
|
||||||
|
<br/>
|
||||||
|
<Form.Group controlId="tage">
|
||||||
|
<Form.Label>Тэги:</Form.Label>
|
||||||
|
<Form.Control name="tage" value={item.tage} type="text" placeholder="праздник программирование" required onChange={handleChange}/>
|
||||||
|
</Form.Group>
|
||||||
|
<br/>
|
||||||
|
<Form.Label>Изображение:</Form.Label>
|
||||||
|
<InputGroup>
|
||||||
|
<Form.Control id="image" type="file" onChange={handleChange}/>
|
||||||
|
</InputGroup>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
NewsForm.propTypes = {
|
||||||
|
item: PropTypes.object,
|
||||||
|
handleChange: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default NewsForm;
|
||||||
|
|
29
src/pages/AdminNews/components/NewsTable.jsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { Table } from "react-bootstrap";
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const NewsTable = ({ children }) => {
|
||||||
|
return (
|
||||||
|
<Table className="mt-4" hover bordered>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope = "col">№</th>
|
||||||
|
<th scope = "col" className ="w-10">Название</th>
|
||||||
|
<th scope = "col" className = "w-30">Дата</th>
|
||||||
|
<th scope = "col" className = "w-50">Описание</th>
|
||||||
|
<th scope = "col">Теги</th>
|
||||||
|
<th scope = "col"></th>
|
||||||
|
<th scope = "col"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{children}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
NewsTable.propTypes = {
|
||||||
|
children : PropTypes.node,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NewsTable;
|
30
src/pages/AdminNews/components/NewsTableRow.jsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { PencilFill, Trash3 } from 'react-bootstrap-icons';
|
||||||
|
|
||||||
|
const NewsTableRow = ({ index, post, onDelete, onEditInPage }) => {
|
||||||
|
const handleAnchorClick = (event, action) => {
|
||||||
|
event.preventDefault();
|
||||||
|
action();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{index}</th>
|
||||||
|
<td>{post.title}</td>
|
||||||
|
<td>{post.date}</td>
|
||||||
|
<td>{post.text}</td>
|
||||||
|
<td>{post.tage}</td>
|
||||||
|
<td><a href="#" onClick={(event) => handleAnchorClick(event, onEditInPage)}><PencilFill /></a></td>
|
||||||
|
<td><a href="#" onClick={(event) => handleAnchorClick(event, onDelete)}><Trash3 /></a></td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
NewsTableRow.propTypes = {
|
||||||
|
index: PropTypes.number,
|
||||||
|
post: PropTypes.object,
|
||||||
|
onDelete: PropTypes.func,
|
||||||
|
onEditInPage: PropTypes.func,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NewsTableRow;
|
34
src/pages/AdminNews/hooks/NewsDeleteHook.jsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import NewsApiService from '../../../components/services/NewsApiService';
|
||||||
|
import useModal from '../../../components/modal/ModalHook';
|
||||||
|
|
||||||
|
const NewsDelete = (usersChangeHandle) => {
|
||||||
|
|
||||||
|
const { isModalShow, showModal, hideModal } = useModal();
|
||||||
|
const [currentId, setCurrentId] = useState(0);
|
||||||
|
|
||||||
|
const showModalDialog = (id) => {
|
||||||
|
showModal();
|
||||||
|
setCurrentId(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
hideModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDelete = async () => {
|
||||||
|
console.log('Новость удалена' + currentId);
|
||||||
|
await NewsApiService.delete(currentId);
|
||||||
|
usersChangeHandle();
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
isDeleteModalShow: isModalShow,
|
||||||
|
showDeleteModal: showModalDialog,
|
||||||
|
handleDeleteConfirm: onDelete,
|
||||||
|
handleDeleteCancel: onClose,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NewsDelete;
|
38
src/pages/AdminNews/hooks/NewsFormHook.jsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import useModal from '../../../components/modal/ModalHook';
|
||||||
|
import NewsItemForm from './NewsItemFormHook';
|
||||||
|
|
||||||
|
const NewsFormModal = (handleLinesChange) => {
|
||||||
|
const { isModalShow, showModal, hideModal } = useModal();
|
||||||
|
const [currentId, setCurrentId] = useState(0);
|
||||||
|
const { item, validated, handleSubmit, handleChange, resetValidity, } = NewsItemForm(currentId, handleLinesChange);
|
||||||
|
|
||||||
|
const showModalDialog = (id) => {
|
||||||
|
setCurrentId(id);
|
||||||
|
resetValidity();
|
||||||
|
showModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
setCurrentId(-1);
|
||||||
|
hideModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async (event) => {
|
||||||
|
if (await handleSubmit(event)) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
isFormModalShow: isModalShow,
|
||||||
|
isFormValidated: validated,
|
||||||
|
showFormModal: showModalDialog,
|
||||||
|
currentItem: item,
|
||||||
|
handleItemChange: handleChange,
|
||||||
|
handleFormSubmit: onSubmit,
|
||||||
|
handleFormClose: onClose,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NewsFormModal;
|
84
src/pages/AdminNews/hooks/NewsItemFormHook.jsx
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import NewsItem from './NewsItemHook.jsx';
|
||||||
|
import NewsApiService from '../../../components/services/NewsApiService';
|
||||||
|
import getBase64 from '../../../components/utils/Base64.js';
|
||||||
|
|
||||||
|
const NewsItemForm = (id, usersChangeHandle) => {
|
||||||
|
const { item, setItem } = NewsItem(id);
|
||||||
|
const [validated, setValidated] = useState(false);
|
||||||
|
|
||||||
|
const resetValidity = () => {
|
||||||
|
setValidated(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNewsObject = (formData) => {
|
||||||
|
const title = formData.title;
|
||||||
|
const date = formData.date;
|
||||||
|
const tage = formData.tage;
|
||||||
|
const text = formData.text;
|
||||||
|
const image = formData.image.startsWith('data:image') ? formData.image : '';
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: title.toString(),
|
||||||
|
date: date.toString(),
|
||||||
|
tage: tage.toString(),
|
||||||
|
text: text.toString(),
|
||||||
|
image,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (event) => {
|
||||||
|
if (event.target.type === 'file') {
|
||||||
|
handleImageChange(event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const inputName = event.target.name;
|
||||||
|
const inputValue = event.target.value;
|
||||||
|
setItem({
|
||||||
|
...item,
|
||||||
|
[inputName]: inputValue,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleImageChange = async (event) => {
|
||||||
|
const { files } = event.target;
|
||||||
|
const file = await getBase64(files.item(0));
|
||||||
|
setItem({
|
||||||
|
...item,
|
||||||
|
image: file,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (event) => {
|
||||||
|
const form = event.currentTarget;
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
const body = getNewsObject(item);
|
||||||
|
|
||||||
|
if (form.checkValidity()) {
|
||||||
|
|
||||||
|
if (id === undefined || id === 0) {
|
||||||
|
await NewsApiService.create(body);
|
||||||
|
} else {
|
||||||
|
await NewsApiService.update(id, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (usersChangeHandle)
|
||||||
|
usersChangeHandle();
|
||||||
|
console.log('Новость сохранена');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
setValidated(true);
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
item,
|
||||||
|
validated,
|
||||||
|
handleSubmit,
|
||||||
|
handleChange,
|
||||||
|
resetValidity,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NewsItemForm;
|
36
src/pages/AdminNews/hooks/NewsItemHook.jsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import NewsApiService from '../../../components/services/NewsApiService';
|
||||||
|
|
||||||
|
const NewsItem = (id) => {
|
||||||
|
const emptyItem = {
|
||||||
|
id: '',
|
||||||
|
title: '',
|
||||||
|
date: '',
|
||||||
|
tage: '',
|
||||||
|
text: '',
|
||||||
|
image: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const [item, setItem] = useState({ ...emptyItem });
|
||||||
|
|
||||||
|
const getItem = async (itemId = undefined) => {
|
||||||
|
if (itemId && itemId > 0) {
|
||||||
|
const data = await NewsApiService.get(itemId);
|
||||||
|
setItem(data);
|
||||||
|
} else {
|
||||||
|
setItem({ ...emptyItem });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
console.log(item);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getItem(id);
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
item,
|
||||||
|
setItem,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NewsItem;
|
23
src/pages/AdminNews/hooks/useLines.jsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import NewsApiService from '../../../components/services/NewsApiService';
|
||||||
|
|
||||||
|
const usePosts = () => {
|
||||||
|
const [linesRefresh, setLinesRefresh] = useState(false);
|
||||||
|
const [lines, setLines] = useState([]);
|
||||||
|
const handleLinesChange = () => setLinesRefresh(!linesRefresh);
|
||||||
|
|
||||||
|
const getLines = async () => {
|
||||||
|
const data = await NewsApiService.getAll();
|
||||||
|
data.reverse();
|
||||||
|
setLines(data ?? []);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => { getLines(); }, [linesRefresh]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
lines,
|
||||||
|
handleLinesChange
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default usePosts;
|
30
src/pages/Contacts/Contacts.jsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { Container, Row, Col } from "react-bootstrap";
|
||||||
|
import imgLetter from "./assets/letter.jpg";
|
||||||
|
import ContactsForm from './components/ContactsForm';
|
||||||
|
|
||||||
|
const Contacts = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Container >
|
||||||
|
<Row className="justify-content-md-center" sm={1} lg={2}>
|
||||||
|
<Col>
|
||||||
|
<div className="text-center">
|
||||||
|
<h3>Обратная связь</h3>
|
||||||
|
Вы можете задать нам любой вопрос, обратиться с предложением или сообщить о проблеме. Для этого заполните приведённую форму.
|
||||||
|
<br/>
|
||||||
|
Мы с радостью вам ответим!
|
||||||
|
<br/><br/>
|
||||||
|
<img src={imgLetter} height="220px" alt="Здание университета"/>
|
||||||
|
<br/>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
<Col>
|
||||||
|
<ContactsForm/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Container>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Contacts;
|
BIN
src/pages/Contacts/assets/letter.jpg
Normal file
After Width: | Height: | Size: 160 KiB |
41
src/pages/Contacts/components/ContactsForm.jsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { Form, FormLabel, Button } from "react-bootstrap";
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import useSubmit from './hooks/ContactsHook.jsx';
|
||||||
|
|
||||||
|
const ContactsForm = () => {
|
||||||
|
const { register, handleSubmit } = useForm();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { onSubmit } = useSubmit();
|
||||||
|
|
||||||
|
const makeSubmit = async (data) => {
|
||||||
|
onSubmit(data);
|
||||||
|
alert('Ваше письмо успешно отправлено!');
|
||||||
|
navigate('/');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form id="contacts-form" onSubmit = {handleSubmit(async (data) => {await makeSubmit(data);})}>
|
||||||
|
<Form.Group controlId="email">
|
||||||
|
<FormLabel>Email:</FormLabel>
|
||||||
|
<Form.Control {...register('email')} type="email" name="email" required />
|
||||||
|
</Form.Group>
|
||||||
|
<br/>
|
||||||
|
<Form.Group controlId="theme">
|
||||||
|
<FormLabel>Тема:</FormLabel>
|
||||||
|
<Form.Control {...register('theme')} type="text" name="theme" required />
|
||||||
|
</Form.Group>
|
||||||
|
<br/>
|
||||||
|
<Form.Group controlId="question">
|
||||||
|
<FormLabel>Ваш вопрос:</FormLabel>
|
||||||
|
<Form.Control {...register('question')} type="text" as="textarea" name="question" rows={4} required />
|
||||||
|
</Form.Group>
|
||||||
|
<br/>
|
||||||
|
<div className="text-center">
|
||||||
|
<Button variant="primary" type="submit">Отправить</Button>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ContactsForm;
|
33
src/pages/Contacts/components/hooks/ContactsHook.jsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import contactApiService from '../services/contactAPIservice.js';
|
||||||
|
|
||||||
|
const useSubmit = () => {
|
||||||
|
|
||||||
|
// id,
|
||||||
|
// email,
|
||||||
|
// theme,
|
||||||
|
// question
|
||||||
|
|
||||||
|
const getNewLetter = (formData) => {
|
||||||
|
const emailt = formData.email;
|
||||||
|
const themet = formData.theme;
|
||||||
|
const questiont = formData.question;
|
||||||
|
|
||||||
|
return {
|
||||||
|
email: emailt,
|
||||||
|
theme: themet,
|
||||||
|
question: questiont,
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async (data) => {
|
||||||
|
console.log(data);
|
||||||
|
const newLetter = getNewLetter(data);
|
||||||
|
const curLetter = await contactApiService.create(newLetter);
|
||||||
|
return 3;
|
||||||
|
};
|
||||||
|
|
||||||
|
return { onSubmit };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useSubmit;
|
@ -0,0 +1,5 @@
|
|||||||
|
import ApiService from '../../../../components/api/contactApiService';
|
||||||
|
|
||||||
|
const contactsAPIservice = new ApiService('letters');
|
||||||
|
|
||||||
|
export default contactsAPIservice;
|
24
src/pages/Errorpage/ErrorPage.jsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import imgError from './assets/cat2.png'
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { Button } from 'react-bootstrap';
|
||||||
|
|
||||||
|
const ErrorPage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<br/><br/>
|
||||||
|
<div className="text-center">
|
||||||
|
<h2>Ой!</h2>
|
||||||
|
<h4>
|
||||||
|
Мы не нашли эту страницу :C
|
||||||
|
</h4>
|
||||||
|
<br/>
|
||||||
|
<img src={imgError} height="80%"/>
|
||||||
|
</div>
|
||||||
|
<Button className="w-10 mt-5" onClick={() => navigate(-1)}>Вернуться назад</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ErrorPage;
|
BIN
src/pages/Errorpage/assets/cat2.png
Normal file
After Width: | Height: | Size: 154 KiB |
9
src/pages/Login/Login.css
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
#image {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 800px) {
|
||||||
|
#image {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
31
src/pages/Login/Login.jsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import "./Login.css";
|
||||||
|
import React from 'react';
|
||||||
|
import { Container, Row, Col } from 'react-bootstrap';
|
||||||
|
import imgLetter from "./assets/students.jpg";
|
||||||
|
import LoginForm from "./components/LoginForm";
|
||||||
|
|
||||||
|
const Login = () => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Container >
|
||||||
|
<Row>
|
||||||
|
<Col sm={12} lg={4} className="me-3">
|
||||||
|
<h3>Вход</h3>
|
||||||
|
<h4>Личный кабинет</h4>
|
||||||
|
<LoginForm/>
|
||||||
|
<h5>Не получается войти?</h5>
|
||||||
|
<h6>Обратитесь к <a href="./contacts">администратору</a>.</h6>
|
||||||
|
<h6>Если у вас ещё нет учётной записи, <a href="./registration">зарегистрируйтесь</a>.</h6>
|
||||||
|
</Col>
|
||||||
|
<Col sm={0} lg={7}>
|
||||||
|
<div id="image" className="text-center">
|
||||||
|
<img src={imgLetter} className="w-100" alt="Здание университета"/>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Container>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
export default Login;
|
BIN
src/pages/Login/assets/students.jpg
Normal file
After Width: | Height: | Size: 67 KiB |
39
src/pages/Login/components/LoginForm.jsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { Form, FormLabel, Button } from 'react-bootstrap';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import useSubmit from './hooks/LoginHook';
|
||||||
|
|
||||||
|
const LoginForm = () => {
|
||||||
|
const { register, handleSubmit} = useForm();
|
||||||
|
const { onSubmit } = useSubmit();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const makeSubmit = async (data) => {
|
||||||
|
const res1 = await onSubmit(data);
|
||||||
|
if (res1 === 1) {
|
||||||
|
alert('Пользователя с таким логином не существует');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (res1 === 2) {
|
||||||
|
alert('Введен неверный пароль');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navigate('/');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form className="mb-5" id="contacts-form" onSubmit={handleSubmit(async (data) => { await makeSubmit(data); })}>
|
||||||
|
<Form.Group controlId="login" className="mb-3">
|
||||||
|
<FormLabel>Логин:</FormLabel>
|
||||||
|
<Form.Control {...register('login')} type="text" name="login" required />
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Group controlId="password">
|
||||||
|
<FormLabel>Пароль:</FormLabel>
|
||||||
|
<Form.Control {...register('password')} type="password" name="password" required />
|
||||||
|
</Form.Group>
|
||||||
|
<Button type="submit" className="mt-4">Войти</Button>
|
||||||
|
</Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoginForm;
|
20
src/pages/Login/components/hooks/LoginHook.jsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import loginApiService from "../service/loginApiService";
|
||||||
|
import useUser from "../../../../providers/hooks/UserHook";
|
||||||
|
|
||||||
|
const useSubmit = () => {
|
||||||
|
const {userLogin} = useUser();
|
||||||
|
const onSubmit = async (data) => {
|
||||||
|
const res1 = await loginApiService.getByLogin(data.login);
|
||||||
|
if (res1.length === 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (res1[0].password !== data.password) {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
userLogin(res1[0]);
|
||||||
|
return 3;
|
||||||
|
};
|
||||||
|
return { onSubmit};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useSubmit;
|
5
src/pages/Login/components/service/loginApiService.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import ApiService from "../../../../components/api/ApiService";
|
||||||
|
|
||||||
|
const loginApiService = new ApiService('users');
|
||||||
|
|
||||||
|
export default loginApiService;
|
36
src/pages/Main/Main.jsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { Carousel } from 'react-bootstrap';
|
||||||
|
import Students1 from './assets/Students1.jpg';
|
||||||
|
import Students2 from './assets/Students2.jpg';
|
||||||
|
import Students3 from './assets/Students3.jpg';
|
||||||
|
|
||||||
|
const Main = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="text-center">
|
||||||
|
<h4>Добро пожаловать на сайт</h4>
|
||||||
|
<h3><b>Государственного политехнического университета</b>!</h3>
|
||||||
|
</div>
|
||||||
|
<div className="text-justify">
|
||||||
|
На нашем сайте вы можете просмотреть <a href="news">последние новости</a>, прочитать <a href="university">информацию о нас</a>, и многое другое....
|
||||||
|
<br/><br/>
|
||||||
|
<Carousel data-bs-theme="dark">
|
||||||
|
<Carousel.Item interval={1800}>
|
||||||
|
<img src={Students1} className="d-block w-80"/>
|
||||||
|
|
||||||
|
</Carousel.Item>
|
||||||
|
<Carousel.Item interval={1800}>
|
||||||
|
<img src={Students2} className="d-block w-80"/>
|
||||||
|
|
||||||
|
</Carousel.Item>
|
||||||
|
<Carousel.Item interval={1800}>
|
||||||
|
<img src={Students3} className="d-block w-80"/>
|
||||||
|
|
||||||
|
</Carousel.Item>
|
||||||
|
</Carousel>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Main;
|
BIN
src/pages/Main/assets/Students1.jpg
Normal file
After Width: | Height: | Size: 46 KiB |
BIN
src/pages/Main/assets/Students2.jpg
Normal file
After Width: | Height: | Size: 132 KiB |
BIN
src/pages/Main/assets/Students3.jpg
Normal file
After Width: | Height: | Size: 132 KiB |
31
src/pages/News/News.jsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import usePosts from "./hooks/usePosts";
|
||||||
|
import PostTemplate from "./components/PostTemplate";
|
||||||
|
import { Form, InputGroup } from "react-bootstrap";
|
||||||
|
|
||||||
|
const News = () => {
|
||||||
|
const { searchTerm, handleChange, searchResults } = usePosts();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="text-center">
|
||||||
|
<h3>Новости</h3>
|
||||||
|
</div>
|
||||||
|
<div className="col-5">
|
||||||
|
<InputGroup className=" mb-3">
|
||||||
|
<InputGroup.Text id="basic-addon1">Поиск новости по тегу:</InputGroup.Text>
|
||||||
|
<Form.Control placeholder="праздник программирование" value={searchTerm} onChange={handleChange} />
|
||||||
|
</InputGroup>
|
||||||
|
</div>
|
||||||
|
<br/>
|
||||||
|
<div className="row row-cols-1 row-cols-md-3 g-4">
|
||||||
|
{
|
||||||
|
searchResults.map((line) =>
|
||||||
|
<PostTemplate key={line.id} post={line}/>)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<br/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default News;
|
BIN
src/pages/News/assets/1september.jpg
Normal file
After Width: | Height: | Size: 1.2 MiB |
BIN
src/pages/News/assets/Halloween.jpg
Normal file
After Width: | Height: | Size: 109 KiB |
BIN
src/pages/News/assets/icpc.jpg
Normal file
After Width: | Height: | Size: 200 KiB |
BIN
src/pages/News/assets/robokross.jpg
Normal file
After Width: | Height: | Size: 209 KiB |
31
src/pages/News/components/PostTemplate.jsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import imgPlaceholder from './assets/200.png';
|
||||||
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
|
|
||||||
|
const PostTemplate = ({ post }) => {
|
||||||
|
return (
|
||||||
|
<div className="col">
|
||||||
|
<div className="card h-100">
|
||||||
|
<img src={post.image || imgPlaceholder} style={{ width: "100%", height: "330px" }} className="card-img-top" alt={post.title}/>
|
||||||
|
<div className="card-body">
|
||||||
|
<h5 className="card-title">{post.title}</h5>
|
||||||
|
<p className="card-text"><small className="text-body-secondary">{post.date}</small></p>
|
||||||
|
<p className="card-text">{post.text}</p>
|
||||||
|
<p className="card-text"><small className="text-body-secondary">
|
||||||
|
{
|
||||||
|
post.tage.split(" ").map((el) => (
|
||||||
|
<i key={el}>{el} </i>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</small></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
PostTemplate.propTypes = {
|
||||||
|
post: PropTypes.object,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PostTemplate;
|
BIN
src/pages/News/components/assets/200.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
39
src/pages/News/hooks/usePosts.jsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import NewsApiService from '../../../components/services/NewsApiService';
|
||||||
|
|
||||||
|
const usePosts = () => {
|
||||||
|
const [linesRefresh, setLinesRefresh] = useState(false);
|
||||||
|
const [lines, setLines] = useState([]);
|
||||||
|
const handleLinesChange = () => setLinesRefresh(!linesRefresh);
|
||||||
|
|
||||||
|
const getLines = async () => {
|
||||||
|
const data = await NewsApiService.getAll();
|
||||||
|
data.reverse();
|
||||||
|
setLines(data ?? []);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// для поиска
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [searchResults, setSearchResults] = useState([]);
|
||||||
|
|
||||||
|
const handleChange = ( event ) => {
|
||||||
|
setSearchTerm(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getLines();
|
||||||
|
const results = lines.filter(line =>
|
||||||
|
line.tage.includes(searchTerm)
|
||||||
|
);
|
||||||
|
setSearchResults(results);
|
||||||
|
}, [searchTerm, linesRefresh]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
searchResults,
|
||||||
|
handleLinesChange,
|
||||||
|
handleChange
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default usePosts;
|
41
src/pages/Profile/Profile.jsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { Container, Row } from 'react-bootstrap';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { useContext } from 'react';
|
||||||
|
import { UserContext } from '../../providers/UserProvider';
|
||||||
|
|
||||||
|
const Profile = () => {
|
||||||
|
|
||||||
|
// зашёл ли пользователь
|
||||||
|
const { user } = useContext(UserContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="text-center">
|
||||||
|
<h3>Профиль</h3>
|
||||||
|
</div>
|
||||||
|
Полная информация о вашем профиле представлена ниже.
|
||||||
|
<br/>
|
||||||
|
<Container>
|
||||||
|
<Row>
|
||||||
|
<div>
|
||||||
|
<b>ID: </b>{user ? user.id : null}
|
||||||
|
<br/>
|
||||||
|
<b>Логин:</b> {user ? user.login : null}
|
||||||
|
<br/>
|
||||||
|
<b>ФИО пользователя:</b> {user ? user.FIO : null}
|
||||||
|
<br/>
|
||||||
|
<b>Электронная почта:</b> {user ? user.email : null}
|
||||||
|
<br/>
|
||||||
|
<b>Дата рождения:</b> {user ? user.birthDate : null}
|
||||||
|
</div>
|
||||||
|
<nav>
|
||||||
|
<Link to = '/'>
|
||||||
|
Вернуться на главную
|
||||||
|
</Link>
|
||||||
|
</nav>
|
||||||
|
</Row>
|
||||||
|
</Container>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
export default Profile;
|
19
src/pages/Registration/Registration.jsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import RegistrationForm from './components/RegistrationForm.jsx';
|
||||||
|
|
||||||
|
const Registration = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="text-center">
|
||||||
|
<h3>Регистрация</h3>
|
||||||
|
Регистрация на нашем сайте позволит Вам стать его полноценным участником.
|
||||||
|
<br/>
|
||||||
|
Вы сможете просматривать расписание групп, свои оценки и учебный план.
|
||||||
|
В случае возникновения проблем с регистрацией, обратитесь к <a href="./contacts">администратору</a>.
|
||||||
|
</div>
|
||||||
|
<br/><br/>
|
||||||
|
<RegistrationForm/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Registration;
|
88
src/pages/Registration/components/RegistrationForm.jsx
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import { Container, Form, Row, Col, Button } from "react-bootstrap";
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import useSubmit from './hooks/RegistrationHook.jsx';
|
||||||
|
|
||||||
|
const RegistrationForm = () => {
|
||||||
|
const { register, handleSubmit } = useForm();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const { onSubmit } = useSubmit();
|
||||||
|
|
||||||
|
const makeSubmit = async (data) => {
|
||||||
|
|
||||||
|
if (data.password !== data.checkPassword) {
|
||||||
|
alert('Пароли не совпадают');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = onSubmit(data);
|
||||||
|
|
||||||
|
if (res === 1) {
|
||||||
|
alert('Пользователь с таким логином уже существует');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (res === 2) {
|
||||||
|
alert('Пользователь с таким email уже существует');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// if (res === 3) {
|
||||||
|
// alert('Повторно введённый пароль не совпадает!');
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
navigate('/registration_success');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form onSubmit = {handleSubmit(async (data) => {await makeSubmit(data);})}>
|
||||||
|
<Container >
|
||||||
|
<Row xs={1} lg={3} className='mb-2'>
|
||||||
|
<Col className='mb-2'>
|
||||||
|
<Form.Group controlId='login'>
|
||||||
|
<Form.Label>Логин: </Form.Label>
|
||||||
|
<Form.Control {...register('login')} type="text" name="login" required />
|
||||||
|
</Form.Group>
|
||||||
|
</Col>
|
||||||
|
<Col className='mb-2'>
|
||||||
|
<Form.Group controlId='FIO'>
|
||||||
|
<Form.Label>ФИО: </Form.Label>
|
||||||
|
<Form.Control {...register('FIO')} type="text" name="FIO" required />
|
||||||
|
</Form.Group>
|
||||||
|
</Col>
|
||||||
|
<Col className='mb-2'>
|
||||||
|
<Form.Group controlId='birthDate'>
|
||||||
|
<Form.Label>Дата рождения: </Form.Label>
|
||||||
|
<Form.Control {...register('birthDate')} type="date" name="birthDate" required />
|
||||||
|
</Form.Group>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row xs={1} lg={3}>
|
||||||
|
<Col className='mb-2'>
|
||||||
|
<Form.Group controlId='email'>
|
||||||
|
<Form.Label>Email: </Form.Label>
|
||||||
|
<Form.Control {...register('email')} type="email" name="email" required />
|
||||||
|
</Form.Group>
|
||||||
|
</Col>
|
||||||
|
<Col className='mb-2'>
|
||||||
|
<Form.Group controlId='password'>
|
||||||
|
<Form.Label>Пароль: </Form.Label>
|
||||||
|
<Form.Control {...register('password')} type="password" name="password" required />
|
||||||
|
</Form.Group>
|
||||||
|
</Col>
|
||||||
|
<Col className='mb-2'>
|
||||||
|
<Form.Group controlId='check-password'>
|
||||||
|
<Form.Label>Проверка пароля: </Form.Label>
|
||||||
|
<Form.Control {...register('checkPassword')} type="password" name="checkPassword" required />
|
||||||
|
</Form.Group>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<br/>
|
||||||
|
</Container>
|
||||||
|
<div className="text-center">
|
||||||
|
<Button variant="primary" type="submit">Зарегистрироваться</Button>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RegistrationForm;
|
40
src/pages/Registration/components/hooks/RegistrationHook.jsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import useUser from '../../../../providers/hooks/UserHook.jsx';
|
||||||
|
import RegApiService from '../services/regAPIservice.js';
|
||||||
|
|
||||||
|
const useSubmit = () => {
|
||||||
|
const { userLogin } = useUser();
|
||||||
|
|
||||||
|
const getNewUser = (formData) => {
|
||||||
|
const logint = formData.login;
|
||||||
|
const FIOt = formData.FIO;
|
||||||
|
const birthDatet = formData.birthDate;
|
||||||
|
const emailt = formData.email;
|
||||||
|
const passwordt = formData.password;
|
||||||
|
|
||||||
|
return {
|
||||||
|
login: logint,
|
||||||
|
FIO: FIOt,
|
||||||
|
birthDate: birthDatet,
|
||||||
|
email: emailt,
|
||||||
|
password: passwordt,
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
const onSubmit = async (data) => {
|
||||||
|
const res1 = await RegApiService.getByLogin(data.login);
|
||||||
|
if (res1.length !== 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const res2 = await RegApiService.getByEmail(data.email);
|
||||||
|
if (res2.length !== 0) {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
const newUser = getNewUser(data);
|
||||||
|
const curUser = await RegApiService.create(newUser);
|
||||||
|
userLogin(curUser);
|
||||||
|
return 3;
|
||||||
|
};
|
||||||
|
return { onSubmit };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useSubmit;
|
@ -0,0 +1,5 @@
|
|||||||
|
import ApiService from '../../../../components/api/ApiService';
|
||||||
|
|
||||||
|
const regAPIservice = new ApiService('users');
|
||||||
|
|
||||||
|
export default regAPIservice;
|
10
src/pages/RegistrationSuccess/RegistrationSuccess.jsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const RegistrationSuccess = () => (
|
||||||
|
<>
|
||||||
|
<div className="text-center">
|
||||||
|
<h3>Регистрация прошла успешно!</h3>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
export default RegistrationSuccess;
|
25
src/pages/University/University.jsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import imgUniversity from "./assets/University.jpg";
|
||||||
|
|
||||||
|
const University = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="text-center">
|
||||||
|
<h3>История ГПУ✨</h3>
|
||||||
|
</div>
|
||||||
|
<p>29 мая 2001 года решением правительства РФ был создан Государственный политехнический университет. Новый ВУЗ был образован на базе Тольяттинского политехнического института и Тольяттинского филиала Самарского государственного педагогического университета.</p>
|
||||||
|
<p>История Политехнического института начинается с вечернего отделения Куйбышевского индустриального института, созданного при Куйбышевгидросторе в 1951 году. Строительство ГЭС остро нуждалось в квалифицированных инженерах. Руководил факультетом Д.Е. Чуркин, а в 1953 году его сменил к.т.н. В.Н. Зубков.</p>
|
||||||
|
<p>1 сентября 1951 года начались занятия 150 студентов. Первое время преподаватели приезжали из Куйбышева, лекции читали многие инженеры-практики со стройки, и сам начальник КГС И.В. Комзин. На отделении было всего два штатных сотрудника: В.Н. Зубков и А.Э. Лившиц. В 1962 году число сотрудников возросло до 90 человек, 8 из них имели ученую степень кандидатов наук.</p>
|
||||||
|
<p>В 1956 году состоялся первый выпуск инженеров-гидростроителей и инженеров-электриков. Постепенно открывались новые специальности, в 1959 году получили дипломы первые инженеры-механики. В 1961 было открыто дневное обучение.</p>
|
||||||
|
<p>В 1961 году были открыты механический факультет (декан В.Н. Зубков) и химико-электротехнический (декан Б.Н. Рачинский). В 1964 году был создан химико-технологический факультет. В 1966 году были выпущены первые инженеры-технологи химического производства. В 1966 году на трех факультетах обучались 2800 студентов.</p>
|
||||||
|
<p>В октябре 1966 года на базе филиала был образован Тольяттинский политехнический институт. Немалую роль в организации института сыграли потребности в кадрах строящегося ВАЗа, а также наличие материальной базы.</p>
|
||||||
|
<p>Первым ректором нового ВУЗа был назначен профессор, доктор технических наук видный ученый из Куйбышева Арон Наумович Резников. Он сумел привлечь новые квалифицированные кадры и создал современный институт с высоким учебным и научным потенциалом. Были созданы 13 новых кафедр, в том числе единственная в стране кафедра промышленной пайки. Благодаря А.Н. Резникову через 10 лет в ТПИ работали 7 докторов наук и 151 кандидат наук. За это время почти в 2 раза возрос набор студентов, и в 10 раз возросли объемы научных работ. Наиболее успешными были 80-е гг., когда учёные ТПИ осваивали в год около 3 млн. руб.</p>
|
||||||
|
<p>Ежегодно ученые института получали десятки свидетельств на изобретения, всего же со времени создания ТПИ было получено 1134 авторских свидетельства на изобретения.</p>
|
||||||
|
<p>ТГУ – современный инновационный вуз. В 2010 году университет стал победителем конкурса Правительства РФ на Премию качества за 2009 год, дважды – в 2004 и 2011 году ТГУ стал лауреатом конкурса «Европейское качество» в группе «100 лучших вузов России». В 2011 году ТГУ завоевал по итогам конкурса СНГ за достижения в области качества оказания услуг приз «Признание делового совершенства». Ректор ТГУ М.М. Криштал в 2011 г. удостоен звания «Российский лидер качества», награждён дипломом и медалью, такую высокую оценку деятельности дала «Всероссийская организация качества».</p>
|
||||||
|
<p>В 2014 году в составе ТГУ 11 институтов, включая институт военного обучения, 44 кафедры, институт дистанционного обучения.</p>
|
||||||
|
<br/>
|
||||||
|
<img src={imgUniversity} height="250px" alt="Здание университета"/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default University;
|
BIN
src/pages/University/assets/University.jpg
Normal file
After Width: | Height: | Size: 150 KiB |
17
src/providers/UserProvider.jsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { createContext, useReducer, useEffect } from 'react';
|
||||||
|
import { loadUser, saveUser, userReducer } from './reducer/UserReduser.jsx';
|
||||||
|
|
||||||
|
export const UserContext = createContext(null);
|
||||||
|
|
||||||
|
export const UserProvider = ({ children }) => {
|
||||||
|
console.log('UserProvider');
|
||||||
|
const [user, dispatch] = useReducer(userReducer, null, loadUser);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
saveUser(user || null);
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
return <UserContext.Provider value = {{ user, dispatch }}>
|
||||||
|
{children}
|
||||||
|
</UserContext.Provider>;
|
||||||
|
};
|
14
src/providers/hooks/UserHook.jsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { useContext } from 'react';
|
||||||
|
import { UserContext } from '../UserProvider.jsx';
|
||||||
|
import { userLogout, userLogin } from '../reducer/UserReduser.jsx';
|
||||||
|
|
||||||
|
const useUser = () => {
|
||||||
|
const { dispatch } = useContext(UserContext);
|
||||||
|
|
||||||
|
return {
|
||||||
|
userLogout: () => dispatch(userLogout()),
|
||||||
|
userLogin: (user) => dispatch(userLogin(user)),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useUser;
|
41
src/providers/reducer/UserReduser.jsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
const USER_KEY = 'user';
|
||||||
|
const USER_LOGIN = 'user/login';
|
||||||
|
const USER_LOGOUT = 'user/logout';
|
||||||
|
|
||||||
|
export const saveUser = (user) => {
|
||||||
|
console.log('User save');
|
||||||
|
localStorage.setItem('user', JSON.stringify(user));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loadUser = (initialValue = []) => {
|
||||||
|
const userData = localStorage.getItem(USER_KEY);
|
||||||
|
console.log('User load: ' + userData.login);
|
||||||
|
if (userData) {
|
||||||
|
return JSON.parse(userData);
|
||||||
|
}
|
||||||
|
return initialValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const userReducer = (prevUser, action) => {
|
||||||
|
console.log(action);
|
||||||
|
const { user } = action;
|
||||||
|
switch (action.type) {
|
||||||
|
case USER_LOGOUT: {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case USER_LOGIN: {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw Error(`Unknown action: ${action.type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const userLogout = () => ({
|
||||||
|
type: USER_LOGOUT,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const userLogin = (user) => ({
|
||||||
|
type: USER_LOGIN, user,
|
||||||
|
});
|
13
vite.config.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
/* eslint-disable import/no-extraneous-dependencies */
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
build: {
|
||||||
|
sourcemap: true,
|
||||||
|
chunkSizeWarningLimit: 1024,
|
||||||
|
emptyOutDir: true,
|
||||||
|
},
|
||||||
|
});
|