This commit is contained in:
frog24 2024-01-22 02:40:32 +04:00
parent 0647d060bb
commit f0a5fbec10
72 changed files with 8197 additions and 0 deletions

24
lab5/all/.eslintrc.cjs Normal file
View File

@ -0,0 +1,24 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'airbnb-base',
'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',
},
}

24
lab5/all/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

8
lab5/all/README.md Normal file
View File

@ -0,0 +1,8 @@
# React + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh

72
lab5/all/data.json Normal file

File diff suppressed because one or more lines are too long

BIN
lab5/all/images/Book.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 KiB

BIN
lab5/all/images/Tolstoy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 MiB

BIN
lab5/all/images/woman.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 833 KiB

15
lab5/all/index.html Normal file
View File

@ -0,0 +1,15 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Library</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
lab5/all/jsconfig.json Normal file
View 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/*"
]
}

View File

@ -0,0 +1,5 @@
{
"static": "./node_modules/json-server/public",
"port": 8081,
"watch": "true"
}

5936
lab5/all/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

38
lab5/all/package.json Normal file
View File

@ -0,0 +1,38 @@
{
"name": "lec4",
"private": true,
"version": "0.0.0",
"type": "module",
"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": {
"axios": "^1.6.5",
"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-hot-toast": "^2.4.1",
"react-router-dom": "^6.18.0"
},
"devDependencies": {
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.15",
"@vitejs/plugin-react": "^4.0.3",
"eslint": "^8.45.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.29.0",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3",
"json-server": "^0.17.4",
"npm-run-all": "^4.1.5",
"vite": "^4.4.5"
}
}

3
lab5/all/public/icon.svg Normal file
View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-book" viewBox="0 0 16 16">
<path d="M1 2.828c.885-.37 2.154-.769 3.388-.893 1.33-.134 2.458.063 3.112.752v9.746c-.935-.53-2.12-.603-3.213-.493-1.18.12-2.37.461-3.287.811zm7.5-.141c.654-.689 1.782-.886 3.112-.752 1.234.124 2.503.523 3.388.893v9.923c-.918-.35-2.107-.692-3.287-.81-1.094-.111-2.278-.039-3.213.492zM8 1.783C7.015.936 5.587.81 4.287.94c-1.514.153-3.042.672-3.994 1.105A.5.5 0 0 0 0 2.5v11a.5.5 0 0 0 .707.455c.882-.4 2.303-.881 3.68-1.02 1.409-.142 2.59.087 3.223.877a.5.5 0 0 0 .78 0c.633-.79 1.814-1.019 3.222-.877 1.378.139 2.8.62 3.681 1.02A.5.5 0 0 0 16 13.5v-11a.5.5 0 0 0-.293-.455c-.952-.433-2.48-.952-3.994-1.105C10.413.809 8.985.936 8 1.783"/>
</svg>

After

Width:  |  Height:  |  Size: 770 B

107
lab5/all/src/App.css Normal file
View File

@ -0,0 +1,107 @@
.Window{
background-color: #A07A54;
margin: auto ;
min-width: 30vw;
}
@media screen and (max-width: 610px) {
.Window{
margin-bottom: 3rem;
margin-left: 2rem;
margin-right: 2rem;
}
}
.a-main:link, .a-main:active, .a-main:hover{
text-decoration: none;
color: #212529;
}
a{
text-decoration: none;
}
body{
background-color: #FFEBCD;
}
.section-name{
background-color: #A07A54;
border-top-left-radius: 30px;
border-top-right-radius: 30px;
}
@media screen and (max-width: 575px) {
.section-name{
width: 70vw;
}
}
@media screen and (max-width: 340px) {
.section-name{
width: 50vw;
font-size: medium;
}
}
@media screen and (max-width: 190px) {
.section-name{
width: 40vw;
font-size: small;
}
}
.form-control, .form-control:focus, .form-select, .form-select:focus{
background-color: #fcf0dc;
border-color: #212529;
border-width: 3px;
box-shadow: none;
font-size: large;
padding: auto;
margin: auto;
}
.form-label{
font-family: Arial;
color: dark;
}
button{
background-color: #613E2B;
color: #FFEBCD;
}
.registerPanel{
max-width: 600px;
}
.bookPanel{
background-color: #D2B48C;
}
.book-name{
background-color: #A07A54;
border-bottom-left-radius: 30px;
border-bottom-right-radius: 30px;
letter-spacing: 2px;
margin-top: -48px;
}
.book-text{
background-color: #A07A54;
border-bottom-left-radius: 30px;
border-bottom-right-radius: 30px;
letter-spacing: 2px;
}
@media screen and (max-width: 250px) {
.book-name{
font-size: small;
}
}
.mini{
max-height: 400px;
}
.table-striped th, tr, td{
background-color: #A07A54 !important;
border-color: #A07A54;
font-size: large;
}

24
lab5/all/src/App.jsx Normal file
View File

@ -0,0 +1,24 @@
import PropTypes from 'prop-types';
import { Container } from 'react-bootstrap';
import { Outlet } from 'react-router-dom';
import './App.css';
import Footer from './components/footer/Footer.jsx';
import Navigation from './components/navigation/Navigation.jsx';
const App = ({ routes }) => {
return (
<>
<Navigation routes={routes}></Navigation>
<Container className='' as="main" fluid>
<Outlet />
</Container>
<Footer />
</>
);
};
App.propTypes = {
routes: PropTypes.array,
};
export default App;

BIN
lab5/all/src/assets/200.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,50 @@
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);
}
// настройка Axios
export const ApiClient = axios.create({
// базовый URL
baseURL: 'http://localhost:8081/',
// максимальное время ожидания
timeout: '3000',
// клиент ожидает получить данные в формате JSON
headers: {
Accept: 'application/json',
},
});
// добавление обработчиков ответов и ошибок сервера
ApiClient.interceptors.response.use(responseHandler, responseErrorHandler);

View File

@ -0,0 +1,35 @@
import { ApiClient } from './ApiClient';
class ApiService {
// коструктор, который устанавливает базовый URL для всех запросов
constructor(url) {
this.url = url;
}
// GET-запрос для получения всех элементов
async getAll(expand) {
return ApiClient.get(`${this.url}${expand || ''}`);
}
// GET-запрос для получения элемента по указанному id
async get(id, expand) {
return ApiClient.get(`${this.url}/${id}${expand || ''}`);
}
// POST-запрос для создания нового элемента
async create(body) {
return ApiClient.post(this.url, body);
}
// PUT-запрос для обновления элемента по указанному id
async update(id, body) {
return ApiClient.put(`${this.url}/${id}`, body);
}
// DELETE-запрос для удаления элемента по указанному id
async delete(id) {
return ApiClient.delete(`${this.url}/${id}`);
}
}
export default ApiService;

View File

@ -0,0 +1,22 @@
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
function Button({ line }) {
return (
<>
<Link className="a-main col-lg-2 col-md-4 col-sm-6 text-center" to={`/BookPage/${line.id}`}>
<div className="text-center border border-4 border-dark">
<img src={line.image} className="img-fluid" alt="Responsive image"></img>
</div>
<h3>{line.title}</h3>
<h3>{line.author}</h3>
</Link>
</>
);
}
Button.propTypes = {
line: PropTypes.object,
};
export default Button;

View File

@ -0,0 +1,3 @@
.my-footer{
background-color: #A07A54;
}

View File

@ -0,0 +1,13 @@
import './Footer.css';
const Footer = () => {
const year = new Date().getFullYear();
return (
<footer className="my-footer border border-dark border-5 mt-auto d-flex flex-shrink-0 justify-content-center align-items-center fs-5">
<b>Барсуков Павел, {year}</b>
</footer>
);
};
export default Footer;

View File

@ -0,0 +1,26 @@
import PropTypes from 'prop-types';
import { Form } from 'react-bootstrap';
const Input = ({
// принимает свойства
name, label, value, onChange, className, ...rest
}) => {
return (
// создание формы ввода исходя из свойств !!!МОЖНО НЕ ИСПОЛЬЗОВАТЬ БУТСТРАП!!!
<Form.Group className= { `mb-2 ${className || ''}` } controlId = {name}>
<Form.Label> {label} </Form.Label>
<Form.Control className='rounded-4' name={name || ''} value={value || ''} onChange={onChange} {...rest} />
</Form.Group>
);
};
// ожидаемые типы свойств
Input.propTypes = {
name: PropTypes.string,
label: PropTypes.string,
value: PropTypes.string,
onChange: PropTypes.func,
className: PropTypes.string,
};
export default Input;

View File

@ -0,0 +1,32 @@
import PropTypes from 'prop-types';
import { Form } from 'react-bootstrap';
const Select = ({
// принимает свойства
values, name, label, value, onChange, className, ...rest
}) => {
// создание выпадающего списка
return (
<Form.Group className={`mb-2 ${className || ''}`} controlId={name}>
<Form.Label className='form-label'>{label}</Form.Label>
<Form.Select name={name || ''} value={value || ''} onChange={onChange} {...rest}>
<option value=''>Choose item</option>
{
values.map((type) => <option key={type.id} value={type.id}>{type.name}</option>)
}
</Form.Select>
</Form.Group>
);
};
// ожидаемые типы свойств
Select.propTypes = {
values: PropTypes.array,
name: PropTypes.string,
label: PropTypes.string,
value: PropTypes.string,
onChange: PropTypes.func,
className: PropTypes.string,
};
export default Select;

View File

@ -0,0 +1,48 @@
import PropTypes from 'prop-types';
import { Button, Form } from 'react-bootstrap';
import { useNavigate } from 'react-router-dom';
import useLinesItemForm from '../hooks/LinesItemFormHook';
import LinesItemForm from './LinesItemForm.jsx';
const LinesForm = ({ id }) => {
const navigate = useNavigate();
const {
item,
validated,
handleSubmit,
handleChange,
} = useLinesItemForm(id);
const onBack = () => {
navigate(-1);
};
const onSubmit = async (event) => {
if (await handleSubmit(event)) {
onBack();
}
};
return (
<>
<Form className='m-0 p-2' noValidate validated={validated} onSubmit={onSubmit}>
<LinesItemForm item={item} handleChange={handleChange} />
<Form.Group className='row justify-content-center m-0 mt-3'>
<Button className='col-5 col-lg-2 m-0 me-2' variant='secondary' onClick={() => onBack()}>
Назад
</Button>
<Button className='col-5 col-lg-2 m-0 ms-2' type='submit' variant='primary'>
Сохранить
</Button>
</Form.Group>
</Form>
</>
);
};
LinesForm.propTypes = {
id: PropTypes.string,
};
export default LinesForm;

View File

@ -0,0 +1,15 @@
#image-preview {
width: 200px;
}
input[type="file"]{
display: none;
}
.fileChoose{
background-color: #fcf0dc;
border-color: #212529;
border-width: 3px;
box-shadow: none;
font-size: large;
padding: auto;
margin: auto;
}

View File

@ -0,0 +1,45 @@
import PropTypes from 'prop-types';
import imgPlaceholder from '../../../assets/200.png';
import Input from '../../input/Input.jsx';
import Select from '../../input/Select.jsx';
import useTypes from '../../types/hooks/TypesHook';
import './LinesItemForm.css';
const LinesItemForm = ({ item, handleChange }) => {
const { types } = useTypes();
return (
<>
<div className='text-center'>
<img id='image-preview' className='rounded' alt='placeholder'
src={item.image || imgPlaceholder} />
</div>
<Select values={types} name='typeId' label='Genre' value={item.typeId} onChange={handleChange}
required />
<Input name='author' label='Author' value={item.author} onChange={handleChange}
type='text' required />
<Input name='title' label='Title' value={item.title} onChange={handleChange}
type='text' required />
<Input name='description' label='Description' value={item.description} onChange={handleChange}
type='text' required />
<div className='fileChoose border border-dark border-3 rounded-2 mt-3 text-center'>
<Input name='image' label='Tap here to choose image' onChange={handleChange}
type='file' accept='image/*' />
</div>
<div className='fileChoose border border-dark border-3 rounded-2 mt-3 text-center'>
<Input name='text' label='Tap here to choose text' onChange={handleChange}
type='file' accept='text/*' />
</div>
</>
);
};
LinesItemForm.propTypes = {
item: PropTypes.object,
handleChange: PropTypes.func,
};
export default LinesItemForm;

View File

@ -0,0 +1,37 @@
import { useState } from 'react';
import toast from 'react-hot-toast';
import useModal from '../../modal/ModalHook';
import LinesApiService from '../service/LinesApiService';
const useLinesDeleteModal = (linesChangeHandle) => {
const { isModalShow, showModal, hideModal } = useModal();
const [currentId, setCurrentId] = useState(0);
const showModalDialog = (id) => {
showModal();
setCurrentId(id);
};
const onClose = () => {
hideModal();
};
// удаление по id, обновление, уведомление
const onDelete = async () => {
await LinesApiService.delete(currentId);
linesChangeHandle();
toast.success('Элемент успешно удален', { id: 'LinesTable' });
onClose();
};
return {
isDeleteModalShow: isModalShow,
showDeleteModal: showModalDialog,
handleDeleteConfirm: onDelete,
handleDeleteCancel: onClose,
};
};
export default useLinesDeleteModal;
// удаление линии

View File

@ -0,0 +1,28 @@
import { useSearchParams } from 'react-router-dom';
import useTypes from '../../types/hooks/TypesHook';
const useTypeFilter = () => {
const filterName = 'type';
const [searchParams, setSearchParams] = useSearchParams();
const { types } = useTypes();
const handleFilterChange = (event) => {
const type = event.target.value;
if (type) {
searchParams.set(filterName, event.target.value);
} else {
searchParams.delete(filterName);
}
setSearchParams(searchParams);
};
return {
types,
currentFilter: searchParams.get(filterName) || '',
handleFilterChange,
};
};
export default useTypeFilter;

View File

@ -0,0 +1,74 @@
import { useState } from 'react';
import useModal from '../../modal/ModalHook';
import useLinesItemForm from './LinesItemFormHook';
const useLinesFormModal = (linesChangeHandle) => {
// получение состояния и функций для модального окна
const { isModalShow, showModal, hideModal } = useModal();
// отслеживание текущего Id
const [currentId, setCurrentId] = useState(0);
// управления формой элемента линии
const {
// состояния линии и валидации
item,
validated,
// функция для отправки
handleSubmit,
// функция для изменения
handleChange,
// функция для сброса состояния валидации
resetValidity,
} = useLinesItemForm(currentId, linesChangeHandle);
const showModalDialog = (id) => {
// устанавливает id в CurrentId
setCurrentId(id);
// сброс валидации
resetValidity();
// показ модального окна
showModal();
};
const onClose = () => {
// устанавливает -1 в CurrentId
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 useLinesFormModal;
// показывает или закрывает модальное окно при надобности

View File

@ -0,0 +1,45 @@
import { useEffect, useState } from 'react';
import LinesApiService from '../service/LinesApiService';
const useLines = (typeFilter) => {
// состояние для перезапроса данных о lines
const [linesRefresh, setLinesRefresh] = useState(false);
// состояние для данных о lines
const [lines, setLines] = useState([]);
// функция которая инфертирует состояние linesRefresh
const handleLinesChange = () => setLinesRefresh(!linesRefresh);
const getLines = async () => {
// параметр запроса для расширения информации, прибавляется к URL
let expand = '?_expand=type';
// если фильтр задан, то добавляется еще один параметр к запросу
if (typeFilter) {
expand = `${expand}&typeId=${typeFilter}`;
}
// вызывает getAll с дополнительными запросами
const data = await LinesApiService.getAll(expand);
// обнавляется состояние lines, если с data что-то не так, то ставится пустой массив
setLines(data ?? []);
};
// вызов getLines при обновлении linesRefresh или typeFilter
useEffect(() => {
getLines();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [linesRefresh, typeFilter]);
// возвращает данные о lines и функцию для изменения состояния
return {
lines,
handleLinesChange,
};
};
export default useLines;
// этот хук в зависимости от typeFilter обновляет lines

View File

@ -0,0 +1,148 @@
import { useState } from 'react';
import toast from 'react-hot-toast';
import getBase64FromFile from '../../utils/Base64';
import LinesApiService from '../service/LinesApiService';
import useLinesItem from './LinesItemHook';
const useLinesItemForm = (id, linesChangeHandle) => {
// получение линии по id
const { item, setItem } = useLinesItem(id);
// параметр правильности заполнения и состояние
const [validated, setValidated] = useState(false);
const [setMessage] = useState();
// сброс состояния
const resetValidity = () => {
setValidated(false);
};
// создание линии из данных формы
const getLineObject = (formData) => {
// преобразование полученных данных
const typeId = parseInt(formData.typeId, 10);
const author = formData.author.toString();
const title = formData.title.toString();
const description = formData.description.toString();
// const sum = parseFloat(price * count).toFixed(2);
// проверяет, начинается ли строка image с data:image
const image = formData.image.startsWith('data:image') ? formData.image : '';
const text = formData.text.toString();
// возврат с преобразованием с строку
return {
typeId: typeId.toString(),
author: author.toString(),
title: title.toString(),
description: description.toString(),
// sum: sum.toString(),
image,
text,
};
};
// изменение изображения
const handleImageChange = async (event) => {
// Извлекает файлы, выбранные пользователем
// обновление image в конкретном item
if (event.target.name === 'image') {
const { files } = event.target;
const file = await getBase64FromFile(files.item(0));
setItem({
...item,
image: file,
});
} else {
const file = event.target.files[0];
const reader = new FileReader();
reader.onload = (e) => {
console.log(e.target.result);
setItem({
...item,
text: e.target.result,
});
};
reader.onerror = (e) => {
console.error('Ошибка при чтении файла:', e);
};
reader.readAsText(file);
}
};
const handleChange = (event) => {
// если изменен файл то вызывается handleImageChange
if (event.target.type === 'file') {
handleImageChange(event);
return;
}
// Получает имя измененного поля
const inputName = event.target.name;
// Получает значение измененного поля
const inputValue = event.target.type === 'checkbox' ? event.target.checked : event.target.value;
// Обновляет состояние элемента линии
setItem({
...item,
[inputName]: inputValue,
});
};
const handleMessageChange = async (event) => {
setMessage(event.target.value);
};
const handleSubmit = async (event) => {
// Получает текущую форму из события.
const form = event.currentTarget;
// Отменяет стандартное поведение формы, предотвращая её автоматическую перезагрузку
event.preventDefault();
event.stopPropagation();
// Получает объект для отправки на сервер
const body = getLineObject(item);
// если форма валидна
if (form.checkValidity()) {
// если нет id то созданиен новой линии иначе обновление старой
if (id === undefined) {
await LinesApiService.create(body);
} else {
await LinesApiService.update(id, body);
}
if (linesChangeHandle) linesChangeHandle();
// уведомление о успешном сохранении
toast.success('Элемент успешно сохранен', { id: 'LinesTable' });
return true;
}
setValidated(true);
return false;
};
return {
// состояня элемента и валидации
item,
validated,
// отправка формы
handleSubmit,
// изменение формы
handleChange,
// сброс состояния валидности
resetValidity,
handleMessageChange,
};
};
export default useLinesItemForm;
// этот хук либо создает либо обновляет линию по id

View File

@ -0,0 +1,47 @@
import { useEffect, useState } from 'react';
import LinesApiService from '../service/LinesApiService';
const useLinesItem = (id) => {
// начальное значение объекта
const emptyItem = {
id: '',
typeId: '',
author: '',
title: '',
description: '',
image: '',
text: '',
};
// item - текущая линия, а setItem - состояние для обновления
const [item, setItem] = useState({ ...emptyItem });
// itemId по умолчанию установлен в undefined
const getItem = async (itemId = undefined) => {
// Если itemId положителен и не является undefined
if (itemId && itemId > 0) {
// Запрос к API
const data = await LinesApiService.get(itemId);
// установка состояния
setItem(data);
} else {
setItem({ ...emptyItem });
}
};
// вызов getItem при обновлении id
useEffect(() => {
getItem(id);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id]);
// возвращает данные о одной линии и функцию для изменения состояния
return {
item,
setItem,
};
};
export default useLinesItem;
// выбор из lines по id

View File

@ -0,0 +1,9 @@
import ApiService from '../../api/ApiService';
// создание экземпляра ApiService по конструктору
const LinesApiService = new ApiService('lines');
export default LinesApiService;
// то есть здесь мы создали специальный объект чтобы делать
// операции, реализованные в ApiService только для lines

View File

@ -0,0 +1,4 @@
button{
background-color: #613E2B;
color: #FFEBCD;
}

View File

@ -0,0 +1,67 @@
import Select from '../../input/Select.jsx';
import ModalConfirm from '../../modal/ModalConfirm.jsx';
import ModalForm from '../../modal/ModalForm.jsx';
import LinesItemForm from '../form/LinesItemForm.jsx';
import useLinesDeleteModal from '../hooks/LinesDeleteModalHook';
import useTypeFilter from '../hooks/LinesFilterHook';
import useLinesFormModal from '../hooks/LinesFormModalHook';
import useLines from '../hooks/LinesHook';
import LinesTable from './LinesTable.jsx';
import LinesTableRow from './LinesTableRow.jsx';
import './Lines.css';
const Lines = () => {
const { types, currentFilter, handleFilterChange } = useTypeFilter();
const { lines, handleLinesChange } = useLines(currentFilter);
const {
isDeleteModalShow,
showDeleteModal,
handleDeleteConfirm,
handleDeleteCancel,
} = useLinesDeleteModal(handleLinesChange);
const {
isFormModalShow,
isFormValidated,
showFormModal,
currentItem,
handleItemChange,
handleFormSubmit,
handleFormClose,
} = useLinesFormModal(handleLinesChange);
return (
<>
<button className='button border border-4 border-dark rounded-4 mb-1 py-2 px-3' onClick={() => showFormModal()}>
Add book
</button>
<Select className='mt-2' values={types} label='Фильтр по товарам'
value={currentFilter} onChange={handleFilterChange} />
<LinesTable>
{
lines.map((line, index) =>
<LinesTableRow key={line.id}
index={index} line={line}
onDelete={() => showDeleteModal(line.id)}
onEdit={() => showFormModal(line.id)}
/>)
}
</LinesTable>
<ModalConfirm show={isDeleteModalShow}
onConfirm={handleDeleteConfirm} onClose={handleDeleteCancel}
title='Deleting' message='Do you want to delete that book?' />
<ModalForm show={isFormModalShow} validated={isFormValidated}
onSubmit={handleFormSubmit} onClose={handleFormClose}
title='Add' >
<LinesItemForm item={currentItem} handleChange={handleItemChange} />
</ModalForm>
</>
);
};
export default Lines;

View File

@ -0,0 +1,28 @@
import PropTypes from 'prop-types';
import { Table } from 'react-bootstrap';
const LinesTable = ({ children }) => {
return (
<Table className='mt-2' striped responsive>
<thead>
<tr>
<th scope='col'></th>
<th scope='col' style={{ width: '33%' }}>Genre</th>
<th scope='col' style={{ width: '33%' }}>Author</th>
<th scope='col' style={{ width: '33%' }} >Title</th>
<th scope='col'></th>
<th scope='col'></th>
</tr>
</thead>
<tbody>
{children}
</tbody >
</Table >
);
};
LinesTable.propTypes = {
children: PropTypes.node,
};
export default LinesTable;

View File

@ -0,0 +1,31 @@
import PropTypes from 'prop-types';
import { PencilFill, Trash3 } from 'react-bootstrap-icons';
const LinesTableRow = ({
index, line, onDelete, onEdit,
}) => {
const handleAnchorClick = (event, action) => {
event.preventDefault();
action();
};
return (
<tr>
<th scope="row">{index + 1}</th>
<td>{line.type.name}</td>
<td>{line.author}</td>
<td>{line.title}</td>
<td><a href="#" onClick={(event) => handleAnchorClick(event, onEdit)}><PencilFill color="black"/></a></td>
<td><a href="#" onClick={(event) => handleAnchorClick(event, onDelete)}><Trash3 color="black" /></a></td>
</tr>
);
};
LinesTableRow.propTypes = {
index: PropTypes.number,
line: PropTypes.object,
onDelete: PropTypes.func,
onEdit: PropTypes.func,
};
export default LinesTableRow;

View File

@ -0,0 +1,12 @@
.Window{
background-color: #A07A54;
margin: auto ;
min-width: 30vw;
}
@media screen and (max-width: 610px) {
.Window{
margin-bottom: 3rem;
margin-left: 2rem;
margin-right: 2rem;
}
}

View File

@ -0,0 +1,10 @@
import './BooksPanel.css';
const BooksPanel = () => {
return (
<div className="window row mx-4 pt-4 border border-5 border-dark rounded-5">
</div>
);
};
export default BooksPanel;

View File

@ -0,0 +1,26 @@
.modalMain{
background-color: #FFEBCD;
}
.modalHeader{
background-color: #A07A54;
}
.modalFooter{
background-color: #A07A54;
}
.fileChoose{
background-color: #fcf0dc;
border-color: #212529;
border-width: 3px;
box-shadow: none;
font-size: large;
padding: auto;
margin: auto;
}
button{
background-color: #613E2B;
color: #FFEBCD;
}
.modalWin{
border-radius: 27px !important;
border-color: aqua !important;
}

View File

@ -0,0 +1,42 @@
import PropTypes from 'prop-types';
import { Modal } from 'react-bootstrap';
import { createPortal } from 'react-dom';
import './Modal.css';
const ModalConfirm = ({
show, title, message, onConfirm, onClose,
}) => {
return createPortal(
<Modal show={show} backdrop='static' className='modalWin' onHide={() => onClose()}>
<Modal.Header className='modalHeader border border-3 border-dark border-bottom-0' closeButton>
<Modal.Title>{title}</Modal.Title>
</Modal.Header>
<Modal.Body className='modalMain border border-3 border-dark'>
{message}
</Modal.Body>
<Modal.Footer className='modalFooter border border-3 border-dark border-top-0 justify-content-center'>
<button className='col-5 border border-4 border-dark rounded-4 mb-1 py-2 px-3'
onClick={() => onClose()}>
No
</button>
<button className='col-5 border border-4 border-dark rounded-4 mb-1 py-2 px-3'
onClick={() => onConfirm()}>
Yes
</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;

View File

@ -0,0 +1,48 @@
import PropTypes from 'prop-types';
import { Form, Modal } from 'react-bootstrap';
import { createPortal } from 'react-dom';
import './Modal.css';
const ModalForm = ({
show, title, validated, onSubmit, onClose, children,
}) => {
return createPortal(
<Modal show={show} backdrop='static' onHide={() => onClose()}>
<Modal.Header className='modalHeader border border-3 border-dark border-bottom-0' closeButton>
<Modal.Title>{title}</Modal.Title>
</Modal.Header>
<Form className='m-0' noValidate validated={validated} onSubmit={onSubmit}>
<Modal.Body className='modal-body modalMain border border-3 border-dark'>
{children}
</Modal.Body>
<Modal.Footer className='modalFooter border border-3 border-dark border-top-0 justify-content-center'>
<button className='col-5 border border-4 border-dark rounded-4 mb-1 py-2 px-3'
onClick={() => onClose()}>
Back
</button>
<button className='col-5 border border-4 border-dark rounded-4 mb-1 py-2 px-3' type='submit'>
Save
</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;

View File

@ -0,0 +1,27 @@
import { useState } from 'react';
const useModal = () => {
// состояние показа модального окна
const [showModal, setShowModal] = useState(false);
// высталение setShowModal в true
const showModalDialog = () => {
setShowModal(true);
};
// высталение setShowModal в false
const hideModalDialog = () => {
setShowModal(false);
};
// возвращает текущее состояние и функции для управления модальном окном
return {
isModalShow: showModal,
showModal: showModalDialog,
hideModal: hideModalDialog,
};
};
export default useModal;
// нужен чтобы управлять модальным окном

View File

@ -0,0 +1,15 @@
.my-header{
background-color: #A07A54;
}
.navbar-text{
color: #212529;
font-size: 27px;
margin-left: 5px;
}
.loginPanel{
margin-right: 2 !important;
}
.NavRight{
justify-content: end;
}

View File

@ -0,0 +1,59 @@
import PropTypes from 'prop-types';
import { Container, Nav, Navbar } from 'react-bootstrap';
import './Navigation.css';
import { Link, useLocation } from 'react-router-dom';
const Navigation = ({ routes }) => {
const location = useLocation();
const pages = routes.filter((route) => Object.prototype.hasOwnProperty.call(route, 'title'));
let userId = localStorage.getItem('EnabledUser');
return (
<header className='border border-5 border-dark my-header mb-5'>
<Navbar className='navbar p-2' expand='md'>
<Container fluid>
<Navbar.Toggle aria-controls='main-navbar' className='m-3'/>
<b>
<Navbar.Collapse id='main-navbar'>
<Nav className='me-5 link' activeKey={location.pathname}>
{
pages.slice(0, pages.length - 1).map((page) =>
<Nav.Link className=' navbar-text' as={Link} key={page.path} eventKey={page.path} to={page.path ?? '/'}>
{page.title}
</Nav.Link>)
}
</Nav>
</Navbar.Collapse>
</b>
<b>
<Navbar.Collapse id='main-navbar-right' className='NavRight'>
{userId === '1' && (
<Nav.Link className=' navbar-text' as={Link} to="/AdminPage">
Admin
</Nav.Link>
)}
<Nav className='me-5 link loginPanel' activeKey={location.pathname}>
<div className='d-flex'>
<Nav.Link className=' navbar-text' as={Link} key={pages[pages.length - 1].path} to={pages[pages.length - 1].path ?? '/'}>
{pages[pages.length - 1].title}
</Nav.Link>
</div>
</Nav>
</Navbar.Collapse>
</b>
</Container>
</Navbar >
</header>
);
};
Navigation.propTypes = {
routes: PropTypes.array,
};
export default Navigation;

View File

@ -0,0 +1,33 @@
import { useEffect, useState } from 'react';
import TypesApiService from '../../user/service/UsersApiService';
const usePersonL = (loginEnter) => {
let login = loginEnter;
const [user, setUser] = useState(null);
const getUser = async () => {
const data = await TypesApiService.getAll(`?mail=${login}`);
if (data && data.length > 0) {
setUser(data[0]);
} else {
setUser(null);
}
};
useEffect(() => {
getUser(login);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handlerLoginChanged = (newLogin) => {
login = newLogin;
getUser();
};
return {
user,
handlerLoginChanged,
};
};
export default usePersonL;

View File

@ -0,0 +1,32 @@
import { useEffect, useState } from 'react';
import TypesApiService from '../service/TypesApiService';
const useType = (id) => {
const emptyItem = {
username: '',
password: '',
email: '',
};
const [type, setType] = useState({ ...emptyItem });
const getType = async (Tid = undefined) => {
if (Tid && Tid > 0) {
const data = await TypesApiService.get(Tid);
setType(data);
} else {
setType({ ...emptyItem });
}
};
useEffect(() => {
getType(id);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id]);
return {
type,
setType,
};
};
export default useType;

View File

@ -0,0 +1,23 @@
import { useEffect, useState } from 'react';
import TypesApiService from '../service/TypesApiService';
const useTypes = () => {
const [types, setTypes] = useState([]);
const getTypes = async () => {
const data = await TypesApiService.getAll();
setTypes(data ?? []);
};
useEffect(() => {
getTypes();
}, []);
return {
types,
};
};
export default useTypes;
// берет данные с сервера о типах

View File

@ -0,0 +1,67 @@
import { useState } from 'react';
import toast from 'react-hot-toast';
import LinesApiService from '../service/TypesApiService';
import useLinesItem from './TypeHook';
const useTypesItemForm = (id, linesChangeHandle) => {
const { type, setType } = useLinesItem(id);
const [validated, setValidated] = useState(false);
const [setMessage] = useState();
const resetValidity = () => {
setValidated(false);
};
const getTypeObject = (formData) => {
const username = formData.username.toString();
const email = formData.email.toString();
const password = formData.password.toString();
return {
username,
email,
password,
};
};
const handleChange = (event) => {
const inputName = event.target.name;
const inputValue = event.target.type === 'checkbox' ? event.target.checked : event.target.value;
setType({
...type,
[inputName]: inputValue,
});
};
const handleMessageChange = async (event) => {
setMessage(event.target.value);
};
const handleSubmit = async (event) => {
const form = event.currentTarget;
event.preventDefault();
event.stopPropagation();
const body = getTypeObject(type);
if (form.checkValidity()) {
if (id === undefined) {
await LinesApiService.create(body);
}
if (linesChangeHandle) linesChangeHandle();
toast.success('Элемент успешно сохранен', { id: 'LinesTable' });
return true;
}
setValidated(true);
return false;
};
return {
type,
validated,
handleSubmit,
handleChange,
resetValidity,
handleMessageChange,
};
};
export default useTypesItemForm;

View File

@ -0,0 +1,8 @@
import ApiService from '../../api/ApiService';
const TypesApiService = new ApiService('types');
export default TypesApiService;
// то есть здесь мы создали специальный объект чтобы делать
// операции, реализованные в ApiService только для types

View File

@ -0,0 +1,45 @@
import PropTypes from 'prop-types';
import { Form } from 'react-bootstrap';
import { useNavigate } from 'react-router-dom';
import useLinesItemForm from '../hooks/UsersItemFormHook';
import UsersItemForm from './UsersItemForm.jsx';
const UsersForm = ({ id }) => {
const navigate = useNavigate();
const {
user,
validated,
handleSubmit,
handleChange,
} = useLinesItemForm(id);
const onBack = () => {
navigate(-1);
};
const onSubmit = async (event) => {
if (await handleSubmit(event)) {
onBack();
}
};
return (
<>
<Form className='container-fluid w-2 register-table text-center' noValidate validated={validated} onSubmit={onSubmit}>
<UsersItemForm item={user} handleChange={handleChange} />
<button type='submit' className='border border-5 border-dark rounded-4' onClick={() => onSubmit()}>
<h3 className="mx-3">
Register
</h3>
</button>
</Form>
</>
);
};
UsersForm.propTypes = {
id: PropTypes.string,
};
export default UsersForm;

View File

@ -0,0 +1,20 @@
import PropTypes from 'prop-types';
import Input from '../../input/Input.jsx';
const LinesItemForm = ({ item, handleChange }) => {
console.log(item);
return (
<>
<Input name='name' label='username' value={item.name} type='text' onChange={handleChange} required />
<Input name='mail' label='email' value={item.mail} type='email' onChange={handleChange} required />
<Input name='password' label='password' value={item.password} onChange={handleChange} type='password' required />
</>
);
};
LinesItemForm.propTypes = {
item: PropTypes.object,
handleChange: PropTypes.func,
};
export default LinesItemForm;

View File

@ -0,0 +1,142 @@
import { useState } from 'react';
import toast from 'react-hot-toast';
import getBase64FromFile from '../../utils/Base64';
import LinesApiService from '../service/UsersApiService';
import useLinesItem from './UsersItemHook';
const useLinesItemForm = (id, linesChangeHandle) => {
// получение линии по id
const { user, setUser } = useLinesItem(id);
// параметр правильности заполнения и состояние
const [validated, setValidated] = useState(false);
const [setMessage] = useState();
// сброс состояния
const resetValidity = () => {
setValidated(false);
};
// создание линии из данных формы
const getLineObject = (formData) => {
// преобразование полученных данных
const name = formData.name.toString();
const mail = formData.mail.toString();
const password = formData.password.toString();
// const sum = parseFloat(price * count).toFixed(2);
// проверяет, начинается ли строка image с data:image
// возврат с преобразованием с строку
return {
name: name.toString(),
mail: mail.toString(),
password: password.toString(),
favorites: [],
};
};
// изменение изображения
const handleImageChange = async (event) => {
// Извлекает файлы, выбранные пользователем
// обновление image в конкретном item
if (event.target.name === 'image') {
const { files } = event.target;
const file = await getBase64FromFile(files.user(0));
setUser({
...user,
image: file,
});
} else {
const file = event.target.files[0];
const reader = new FileReader();
reader.onload = (e) => {
console.log(e.target.result);
setUser({
...user,
text: e.target.result,
});
};
reader.onerror = (e) => {
console.error('Ошибка при чтении файла:', e);
};
reader.readAsText(file);
}
};
const handleChange = (event) => {
// если изменен файл то вызывается handleImageChange
if (event.target.type === 'file') {
handleImageChange(event);
return;
}
// Получает имя измененного поля
const inputName = event.target.name;
// Получает значение измененного поля
const inputValue = event.target.type === 'checkbox' ? event.target.checked : event.target.value;
// Обновляет состояние элемента линии
setUser({
...user,
[inputName]: inputValue,
});
};
const handleMessageChange = async (event) => {
setMessage(event.target.value);
};
const handleSubmit = async (event) => {
// Получает текущую форму из события.
const form = event.currentTarget;
// Отменяет стандартное поведение формы, предотвращая её автоматическую перезагрузку
event.preventDefault();
event.stopPropagation();
// Получает объект для отправки на сервер
const body = getLineObject(user);
// если форма валидна
if (form.checkValidity()) {
// если нет id то созданиен новой линии иначе обновление старой
if (id === undefined) {
await LinesApiService.create(body);
} else {
await LinesApiService.update(id, body);
}
if (linesChangeHandle) linesChangeHandle();
// уведомление о успешном сохранении
toast.success('Элемент успешно сохранен', { id: 'LinesTable' });
return true;
}
setValidated(true);
return false;
};
return {
// состояня элемента и валидации
user,
validated,
// отправка формы
handleSubmit,
// изменение формы
handleChange,
// сброс состояния валидности
resetValidity,
handleMessageChange,
};
};
export default useLinesItemForm;
// этот хук либо создает либо обновляет линию по id

View File

@ -0,0 +1,36 @@
import { useEffect, useState } from 'react';
import UsersApiService from '../service/UsersApiService';
const useUsersItem = (id) => {
const emptyItem = {
name: '',
mail: '',
password: '',
favorites: [],
};
const [user, setUser] = useState({ ...emptyItem });
const getItem = async (itemId = undefined) => {
if (itemId && itemId > 0) {
const data = await UsersApiService.get(itemId);
// console.log(data);
setUser(data);
} else {
setUser({ ...emptyItem });
}
};
useEffect(() => {
getItem(id);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id]);
return {
user,
setUser,
};
};
export default useUsersItem;

View File

@ -0,0 +1,5 @@
import ApiService from '../../api/ApiService';
const UsersApiService = new ApiService('users');
export default UsersApiService;

View File

@ -0,0 +1,18 @@
.Window{
background-color: #A07A54;
margin: auto ;
min-width: 30vw;
}
@media screen and (max-width: 610px) {
.Window{
margin-bottom: 3rem;
margin-left: 2rem;
margin-right: 2rem;
}
}
.mini{
max-height: 400px;
}
.bookPanel{
background-color: #D2B48C;
}

View File

@ -0,0 +1,34 @@
import './UserBook.css';
import { Link } from 'react-router-dom';
const UserBook = () => {
return (
<div className="bookPanel row mx-4 pt-4 border border-5 border-dark rounded-5 p-2 mb-5">
<div className="col-lg-3 col-md-4 col-sm-6 text-center mb-4">
<Link className="link-dark" to="/BookPage">
<div className="text-center">
<img src="images/WarAndPeace.png" className="mini border border-4 border-dark" alt="Responsive image"></img>
</div>
</Link>
</div>
<div className="col-lg-9 col-md-8 col-sm-6">
<div className="Window rounded-4 border border-4 border-dark p-2 mx-3">
<h1 className="display-2">
<Link className="link-dark" to="/BookPage">Title</Link> - <Link className="link-dark" to="/AuthorPage">Author</Link>
</h1>
<h3>Book description Book description Book description Book description
Book description Book description Book description Book description
Book description Book description Book description Book description
Book description Book description Book description Book description
Book description Book description Book description Book description
</h3>
</div>
</div>
</div>
);
};
export default UserBook;

View File

@ -0,0 +1,17 @@
const getBase64FromFile = 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 getBase64FromFile;
// преобразование файла в строку символов

0
lab5/all/src/index.css Normal file
View File

64
lab5/all/src/main.jsx Normal file
View File

@ -0,0 +1,64 @@
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 MainPage from './pages/MainPage.jsx';
import ErrorPage from './pages/ErrorPage.jsx';
import LoginPage from './pages/LoginPage.jsx';
import BookPage from './pages/BookPage.jsx';
import AuthorPage from './pages/AuthorPage.jsx';
import TextPage from './pages/TextPage.jsx';
import ProfilePage from './pages/ProfilePage.jsx';
import AdminPage from './pages/AdminPage.jsx';
const routes = [
{
index: true,
path: '/MainPage',
element: <MainPage />,
title: 'Main',
},
{
path: '/ProfilePage/:id?',
element: <ProfilePage />,
title: 'Profile',
},
{
path: '/LoginPage',
element: <LoginPage />,
title: 'Log in/ Sign up',
},
{
path: '/BookPage/:id?',
element: <BookPage />,
},
{
path: '/AuthorPage',
element: <AuthorPage />,
},
{
path: '/TextPage/:id?',
element: <TextPage />,
},
{
path: '/AdminPage',
element: <AdminPage />,
},
];
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>,
);

View File

@ -0,0 +1,10 @@
// import { Link } from 'react-router-dom';
import Lines from '../components/lines/table/Lines.jsx';
const AdminPage = () => {
return (
<Lines />
);
};
export default AdminPage;

View File

@ -0,0 +1,37 @@
const AuthorPage = () => {
return (
<>
<main>
<div className="bookPanel row mx-4 pt-4 border border-5 border-dark rounded-5 p-2 mb-5">
<div className="col-lg-3 col-md-4 col-sm-6 text-center mb-4">
<div className="text-center">
<img src="images/Tolstoy.png" className="img-fluid border border-4 border-dark" alt="Responsive image"></img>
</div>
</div>
<div className="col-lg-9 col-md-8 col-sm-6 ">
<div className="Window rounded-4 border border-4 border-dark mt-3 mb-5 mx-3 p-2 text-center">
<h2>Author name</h2>
</div>
<div className="Window rounded-4 border border-4 border-dark p-2 mx-3">
<h3>
Biography text Biography text Biography text Biography text
Biography text Biography text Biography text Biography text
Biography text Biography text Biography text Biography text
Biography text Biography text Biography text Biography text
Biography text Biography text Biography text Biography text
Biography text Biography text Biography text Biography text
Biography text Biography text Biography text Biography text
</h3>
</div>
</div>
</div>
</main>
</>
);
};
export default AuthorPage;

View File

@ -0,0 +1,70 @@
import { Link, useParams } from 'react-router-dom';
import useLinesItemForm from '../components/lines/hooks/LinesItemFormHook';
import useType from '../components/types/hooks/TypeHook';
import useUsersItem from '../components/user/hooks/UsersItemHook';
const BookPage = () => {
const { id } = useParams();
const { item } = useLinesItemForm(id);
const { type } = useType(id);
const ID = localStorage.getItem('EnabledUser');
const { user } = useUsersItem(ID);
const addToFavorites = (itemId) => {
if (!user.favorites.includes(itemId)) {
user.favorites.unshift(itemId);
/*
for (let i = 0; i < user.favorites.length; i++) {
console.log(`Element at index ${i}: ${user.favorites[i]}`);
}
*/
}
};
return (
<>
<main className='mb-4'>
<div className="bookPanel row mx-4 pt-4 border border-5 border-dark rounded-5 p-2 mb-5">
<div className="col-lg-3 col-md-4 col-sm-6 text-center mb-4">
<div className="text-center">
<img src={ item.image} className="img-fluid border border-4 border-dark" alt="Responsive image"></img>
</div>
<div className="col mt-3" >
<Link to={`/TextPage/${type.id}`}>
<button className="border border-dark border-5 rounded-4 col-12">
<h3 className="mx-3">
Read
</h3>
</button>
</Link>
<button className="border border-dark border-5 rounded-4 col-12 mt-2">
<h3 className="mx-3" onClick={ () => addToFavorites(item.id) }>
Add to favorotes
</h3>
</button>
</div>
</div>
<div className="col-lg-9 col-md-8 col-sm-6">
<div className="Window rounded-4 border border-4 border-dark mb-5 p-2 text-center mt-3 mx-3">
<h2>
{item.title} - <Link className="link-dark" to="/AuthorPage">{item.author}</Link>
</h2>
</div>
<div className="Window rounded-4 border border-4 border-dark p-2 mx-3">
<h3>
{item.description}
</h3>
</div>
</div>
</div>
</main>
</>
);
};
export default BookPage;

View File

@ -0,0 +1,19 @@
import { Alert, Container } from 'react-bootstrap';
import { useNavigate } from 'react-router-dom';
const ErrorPage = () => {
const navigate = useNavigate();
return (
<Container fluid className="p-2 row justify-content-center">
<Container className='col-md-6'>
<Alert variant="danger" className='text-center'>
Page not
</Alert>
<button className="button border border-4 border-dark rounded-4 mb-1 py-2 px-3" onClick={() => navigate(-1)}>back</button>
</Container>
</Container>
);
};
export default ErrorPage;

View File

@ -0,0 +1,101 @@
// import { Link } from 'react-router-dom';
import { useState } from 'react';
import { Form } from 'react-bootstrap';
import { useParams, useNavigate } from 'react-router-dom';
import UsersForm from '../components/user/form/UsersForm.jsx';
import usePersonL from '../components/types/hooks/LoginHooks';
const LoginPage = () => {
const { id } = useParams();
const navigator = useNavigate();
const [validated, setValidated] = useState(false);
const [formData, setFormData] = useState({
email: '',
password: '',
});
const { user, handlerLoginChanged } = usePersonL(formData.email);
const handleSubmit = (event) => {
const form = event.currentTarget;
event.preventDefault();
event.stopPropagation();
if (form.checkValidity() !== false) {
console.log(formData);
}
if (user === null) {
console.log('NULL!');
return;
}
console.log(user.password, formData.password);
if (user.password === formData.password) {
const ind = user.id;
localStorage.setItem('EnabledUser', JSON.stringify(user.id));
navigator(`/ProfilePage/${ind}`);
}
setValidated(true);
};
const handleChange = (event) => {
const inputName = event.target.name;
const inputValue = event.target.type === 'checkbox' ? event.target.checked : event.target.value;
setFormData({
...formData,
[inputName]: inputValue,
});
if (inputName === 'email') {
handlerLoginChanged(inputValue);
}
};
return (
<>
<main className="Window registerPanel p-4 border border-5 border-dark rounded-5 mt-5">
<div className="row mx-2">
<div className="col-sm">
<h1 className="text-center">
Sign up
</h1>
<UsersForm id={id} />
</div>
<div className="col-sm">
<h1 className="text-center">
Log in
</h1>
<Form className='container-fluid w-2 register-table text-center col' noValidate validated={validated} onSubmit={handleSubmit}>
<Form.Group className='mb-2' controlId='email'>
<Form.Label>email</Form.Label>
<Form.Control type='email' name='email' className='rounded-4' required
value={formData.email} onChange={handleChange} />
</Form.Group>
<Form.Group className='mb-2' controlId='password'>
<Form.Label>password</Form.Label>
<Form.Control type='password' name='password' className='rounded-4' required
value={formData.password} onChange={handleChange} />
</Form.Group>
<div className='text-center'>
<button className='border border-dark border-5 rounded-4' type='submit'>
<h3 className="mx-3">
Login
</h3>
</button>
</div>
</Form>
</div>
</div>
</main>
</>
);
};
export default LoginPage;

View File

@ -0,0 +1,27 @@
import Button from '../components/bookCard/BookCard.jsx';
import useLines from '../components/lines/hooks/LinesHook';
import useTypeFilter from '../components/lines/hooks/LinesFilterHook';
const MainPage = () => {
const { currentFilter } = useTypeFilter();
const { lines } = useLines(currentFilter);
return (
<>
<>
<main className="mb-5">
<h3 className="section-name text-center col-xs-9 col-sm-6 col-mb-1 col-lg-2 justify-content-center mx-auto mb-0 border border-bottom-0 border-3 border-dark">
New:
</h3>
<div className="window row mx-4 pt-4 border border-5 border-dark rounded-5">
{
lines.slice().reverse().map((line) =>
<Button key={line.id} line={line} />)
}
</div>
</main>
</>
</>
);
};
export default MainPage;

View File

@ -0,0 +1,32 @@
import useUsersItem from '../components/user/hooks/UsersItemHook';
import avatar from '../assets/avatar.png';
const ProfilePage = () => {
const id = localStorage.getItem('EnabledUser');
const { user } = useUsersItem(id);
return (
<>
<main>
<div className="bookPanel row mb-5 mx-4 mb-5 p-4 border border-5 border-dark rounded-5">
<div className="col-lg-2 col-md-6 col-sm-12 mb-4">
<img src={avatar} className="img-fluid"></img>
</div>
<div className="col-lg-10 col-md-6 col-sm-12">
<div className="Window border border-5 border-dark rounded-5 ">
<h1 className="display-1 p-2 ms-3">
{user.name}
</h1>
</div>
</div>
</div>
</main>
</>
);
};
export default ProfilePage;

View File

@ -0,0 +1,27 @@
import { Link, useParams } from 'react-router-dom';
import useLinesItemForm from '../components/lines/hooks/LinesItemFormHook';
const TextPage = () => {
const { id } = useParams();
const { item } = useLinesItemForm(id);
return (
<>
<main>
<div className="d-flex justify-content-center">
<div className="book-name d-flex justify-content-center col-5 border border-top-0 border-4 border-dark">
<h1 className="">
<Link className="link-dark" to={`/BookPage/${item.id}`} >{item.title}</Link>
</h1>
</div>
</div>
<div className="Window m-5 border border-5 border-dark rounded-4 p-4">
<h2 className="book-text">
{item.text}
</h2>
</div>
</main>
</>
);
};
export default TextPage;

13
lab5/all/vite.config.js Normal file
View 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,
},
});

Binary file not shown.