This commit is contained in:
MaDernizator 2024-01-10 21:37:47 +04:00
parent 210e7f0c6c
commit fbb5144ef3
72 changed files with 8066 additions and 0 deletions

25
.eslintrc.cjs Normal file
View File

@ -0,0 +1,25 @@
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',
'linebreak-style': 'off',
},
}

24
.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?

52
data.json Normal file

File diff suppressed because one or more lines are too long

BIN
images/Empire.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
images/Tolstoy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
images/WarAndPeace.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

BIN
images/book.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

17
images/text.txt Normal file
View File

@ -0,0 +1,17 @@
Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст
Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст
Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст
Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст
Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст
Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст
Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст
Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст
Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст
Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст
Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст
Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст
Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст
Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст
Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст
Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст
Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст

BIN
images/woman.png Normal file

Binary file not shown.

15
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="/vite.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>

14
jsconfig.json Normal file
View File

@ -0,0 +1,14 @@
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "Node",
"target": "ES2020",
"jsx": "react",
"strictNullChecks": true,
"strictFunctionTypes": true
},
"exclude": [
"node_modules",
"**/node_modules/*"
]
}

5990
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

38
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.2",
"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"
}
}

1
public/vite.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

114
src/App.css Normal file
View File

@ -0,0 +1,114 @@
.Window{
background-color: #6C8DCC;
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: #6C8DCC;
}
.section-name{
background-color: #2F4E89;
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: #6C8DCC;
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: #6C8DCC;
color: #2F4E89;
}
.registerPanel{
max-width: 600px;
}
.bookPanel{
background-color: #2F4E89;
}
.book-name{
background-color: #2F4E89;
border-bottom-left-radius: 30px;
border-bottom-right-radius: 30px;
letter-spacing: 2px;
margin-top: -48px;
}
.book-text{
background-color: #6C8DCC;
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: #6C8DCC !important;
border-color: #A07A54;
font-size: large;
}
#news{
margin-left: 50;
}
#popular{
margin-left: 50;
}

24
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
src/assets/200.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
src/assets/Rick.mp4 Normal file

Binary file not shown.

BIN
src/assets/fon.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

1
src/assets/react.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

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,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:3000/',
timeout: '3000',
headers: {
Accept: 'application/json',
},
});
ApiClient.interceptors.response.use(responseHandler, responseErrorHandler);

View File

@ -0,0 +1,29 @@
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 update(id, body) {
return ApiClient.put(`${this.url}/${id}`, body);
}
async delete(id) {
return ApiClient.delete(`${this.url}/${id}`);
}
}
export default ApiService;

View File

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

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,23 @@
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 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,29 @@
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=''>Выберите значение</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,55 @@
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();
let userId = '';
try {
userId = localStorage.getItem('EnabledUser');
} catch (error) {
userId = null;
}
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} user={userId} />
<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,3 @@
#image-preview {
width: 200px;
}

View File

@ -0,0 +1,27 @@
import PropTypes from 'prop-types';
import imgPlaceholder from '../../../assets/200.png';
import Input from '../../input/Input.jsx';
import './LinesItemForm.css';
const LinesItemForm = ({ item, handleChange }) => {
return (
<>
<div className='text-center'>
<img id='image-preview' className='rounded' alt='placeholder'
src={item.image || imgPlaceholder} />
</div>
<Input name='author' label='Автор' value={item.author} type='text' onChange={handleChange} required />
<Input name='title' label='Название' value={item.title} type='text' onChange={handleChange} required />
<Input name='description' label='Описание' value={item.description} type='text' onChange={handleChange} />
<Input name='image' label='Изображение' onChange={handleChange} type='file' accept='image/*'/>
<Input name='video' label='Видео' onChange={handleChange} type='file' accept='text/*'/>
</>
);
};
LinesItemForm.propTypes = {
item: PropTypes.object,
handleChange: PropTypes.func,
};
export default LinesItemForm;

View File

@ -0,0 +1,34 @@
import { useState } from 'react';
import toast from 'react-hot-toast';
import LinesApiService from '../service/LinesApiService';
import useModal from '../../modal/ModalHook';
const useLinesDeleteModal = (linesChangeHandle) => {
const { isModalShow, showModal, hideModal } = useModal();
const [currentId, setCurrentId] = useState(0);
const showModalDialog = (id) => {
showModal();
setCurrentId(id);
};
const onClose = () => {
hideModal();
};
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,25 @@
import { useEffect, useState } from 'react';
import LinesApiService from '../service/LinesApiService';
const useLines = () => {
const [linesRefresh, setLinesRefresh] = useState(false);
const [lines, setLines] = useState([]);
const handleLinesChange = () => setLinesRefresh(!linesRefresh);
const getLines = async () => {
const data = await LinesApiService.getAll();
setLines(data ?? []);
};
useEffect(() => {
getLines();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [linesRefresh]);
return {
lines,
handleLinesChange,
};
};
export default useLines;

View File

@ -0,0 +1,102 @@
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) => {
const { item, setItem } = useLinesItem(id);
const [validated, setValidated] = useState(false);
const [setMessage] = useState();
const resetValidity = () => {
setValidated(false);
};
const getLineObject = (formData) => {
const author = formData.author.toString();
const title = formData.title.toString();
const description = formData.description.toString();
const image = formData.image.startsWith('data:image') ? formData.image : '';
const video = formData.video.toString();
return {
author,
title,
description,
image,
video,
};
};
const handleImageChange = async (event) => {
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) => {
setItem({
...item,
video: e.target.result,
});
};
reader.onerror = (e) => {
console.error('Ошибка при чтении файла:', e);
};
reader.readAsText(file);
}
};
const handleChange = (event) => {
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()) {
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;

View File

@ -0,0 +1,34 @@
import { useEffect, useState } from 'react';
import LinesApiService from '../service/LinesApiService';
const useLinesItem = (id) => {
const emptyItem = {
author: '',
title: '',
description: '',
image: '',
video: '',
};
const [item, setItem] = useState({ ...emptyItem });
const getItem = async (typeId = undefined) => {
if (typeId && typeId > 0) {
const data = await LinesApiService.get(typeId);
setItem(data);
} else {
setItem({ ...emptyItem });
}
};
useEffect(() => {
getItem(id);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id]);
return {
item,
setItem,
};
};
export default useLinesItem;

View File

@ -0,0 +1,32 @@
import { useEffect, useState } from 'react';
import TypesApiService from '../service/TypesApiService';
const useType = (id) => {
const emptyItem = {
name: '',
password: '',
mail: '',
};
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,5 @@
import ApiService from '../../api/ApiService';
const LinesApiService = new ApiService('lines');
export default LinesApiService;

View File

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

View File

@ -0,0 +1,55 @@
import { Button, ButtonGroup } from 'react-bootstrap';
import { Link, useNavigate } from 'react-router-dom';
import Select from '../../input/Select.jsx';
import useTypeFilter from '../hooks/LinesFilterHook';
import useLines from '../hooks/LinesHook';
import LinesTable from './LinesTable.jsx';
import LinesTableRow from './LinesTableRow.jsx';
import ModalConfirm from '../../modal/ModalConfirm.jsx';
import useLinesDeleteModal from '../hooks/LinesDeleteModalHook';
const Lines = () => {
const { types, currentFilter, handleFilterChange } = useTypeFilter();
const { lines, handleLinesChange } = useLines(currentFilter);
const {
isDeleteModalShow,
showDeleteModal,
handleDeleteConfirm,
handleDeleteCancel,
} = useLinesDeleteModal(handleLinesChange);
const navigate = useNavigate();
const showEditPage = (id) => {
navigate(`/redact/${id}`);
};
return (
<>
<ButtonGroup>
<Button as={Link} to='/redact' variant='success'>
Добавить товар (страница)
</Button>
</ButtonGroup>
<Select className='mt-2' values={types} label='Фильтр по товарам'
value={currentFilter} onChange={handleFilterChange} />
<LinesTable>
{
lines.map((line, index) =>
<LinesTableRow key={line.id}
index={index} line={line}
onEditInPage={() => showEditPage(line.id)}
onDelete={() => showDeleteModal(line.id)}
/>)
}
</LinesTable>
<ModalConfirm show={isDeleteModalShow}
onConfirm={handleDeleteConfirm} onClose={handleDeleteCancel}
title='Удаление' message='Удалить элемент?' />
</>
);
};
export default Lines;

View File

@ -0,0 +1,29 @@
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" className="w-25">Автор</th>
<th scope="col" className="w-25">Название</th>
<th scope="col" className="w-25">Редактировать</th>
<th scope="col" className="w-25">Удалить</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,36 @@
import PropTypes from 'prop-types';
import { PencilSquare, Trash3 } from 'react-bootstrap-icons';
import { useNavigate } from 'react-router-dom';
const LinesTableRow = ({
index, line, onDelete, onEditInPage,
}) => {
const handleAnchorClick = (event, action) => {
event.preventDefault();
action();
};
const navigator = useNavigate();
const video = (id) => {
navigator(`/watch/${id}`);
};
return (
<tr>
<th scope="row"><a onClick={() => video(line.id)} >{index + 1}</a></th>
<td>{line.author}</td>
<td>{line.title}</td>
<td><a href="#" onClick={(event) => handleAnchorClick(event, onEditInPage)}><PencilSquare /></a></td>
<td><a href="#" onClick={(event) => handleAnchorClick(event, onDelete)}><Trash3 /></a></td>
</tr>
);
};
LinesTableRow.propTypes = {
index: PropTypes.number,
line: PropTypes.object,
onDelete: PropTypes.func,
onEdit: PropTypes.func,
onEditInPage: PropTypes.func,
};
export default LinesTableRow;

View File

@ -0,0 +1,55 @@
import PropTypes from 'prop-types';
import { Button, ButtonGroup } from 'react-bootstrap';
import { Link, useNavigate } from 'react-router-dom';
import useLines from '../hooks/LinesHook';
import LinesTable from './LinesTable.jsx';
import LinesTableRow from './LinesTableRow.jsx';
import ModalConfirm from '../../modal/ModalConfirm.jsx';
import useLinesDeleteModal from '../hooks/LinesDeleteModalHook';
const LinesUser = () => {
const idUser = localStorage.getItem('EnabledUser');
const { lines, handleLinesChange } = useLines(idUser);
const {
isDeleteModalShow,
showDeleteModal,
handleDeleteConfirm,
handleDeleteCancel,
} = useLinesDeleteModal(handleLinesChange);
const navigate = useNavigate();
const showEditPage = (id) => {
navigate(`/redact/${id}`);
};
return (
<>
<ButtonGroup>
<Button as={Link} to='/redact' variant='success'>
Добавить товар (страница)
</Button>
</ButtonGroup>
<LinesTable>
{
lines.map((line, index) =>
<LinesTableRow key={line.id}
index={index} line={line}
onEditInPage={() => showEditPage(line.id)}
onDelete={() => showDeleteModal(line.id)}
/>)
}
</LinesTable>
<ModalConfirm show={isDeleteModalShow}
onConfirm={handleDeleteConfirm} onClose={handleDeleteCancel}
title='Удаление' message='Удалить элемент?' />
</>
);
};
LinesUser.propTypes = {
idUser: PropTypes.string,
};
export default LinesUser;

View File

@ -0,0 +1,15 @@
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;

View File

@ -0,0 +1,3 @@
.modal-title {
font-size: 1.2rem;
}

View File

@ -0,0 +1,42 @@
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,
}) => {
return 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 variant='secondary' className='col-5 m-0 me-2'
onClick={() => onClose()}>
Нет
</Button>
<Button variant='primary' 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;

View File

@ -0,0 +1,43 @@
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,
}) => {
return 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 variant='secondary' className='col-5 m-0 me-2'
onClick={() => onClose()}>
Отмена
</Button>
<Button variant='primary' 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;

View 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;

View File

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

View File

@ -0,0 +1,63 @@
import PropTypes from 'prop-types';
import { Container, Nav, Navbar } from 'react-bootstrap';
import './Navigation.css';
import { Link, useLocation, useNavigate } from 'react-router-dom';
const Navigation = ({ routes }) => {
const location = useLocation();
const pages = routes.filter((route) => Object.prototype.hasOwnProperty.call(route, 'title'));
const navigate = useNavigate();
const indexPageLink = routes.filter((route) => route.index === false).shift();
let userId = localStorage.getItem('EnabledUser');
const RedirectChanel = () => {
userId = localStorage.getItem('EnabledUser');
if (userId !== null) {
navigate('/Admin');
}
};
const RedirectSearch = () => {
navigate('/search');
};
const RedirectLogin = () => {
userId = localStorage.getItem('EnabledUser');
if (userId !== null) {
navigate('/account');
} else {
navigate('/login');
}
};
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>)
}
{userId === '1' && (
<Nav.Link className=' navbar-text' as={Link} to="/admin">
Админка
</Nav.Link>
)}
</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 '../service/TypesApiService';
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,33 @@
import { useEffect, useState } from 'react';
import LinesApiService from '../../lines/service/LinesApiService';
const SearchVideos = (text) => {
let search = text;
const [files, setFiles] = useState(null);
const getVideo = async () => {
const data = await LinesApiService.getAll(`?name=${search}`);
if (data && data.length > 0) {
setFiles(data[0]);
} else {
setFiles(null);
}
};
useEffect(() => {
getVideo(search);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handlerVideoSearch = (newText) => {
search = newText;
getVideo();
};
return {
files,
handlerVideoSearch,
};
};
export default SearchVideos;

View File

@ -0,0 +1,32 @@
import { useEffect, useState } from 'react';
import TypesApiService from '../service/TypesApiService';
const useType = (id) => {
const emptyItem = {
name: '',
password: '',
mail: '',
};
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,21 @@
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 name = formData.name.toString();
const mail = formData.mail.toString();
const password = formData.password.toString();
return {
name,
mail,
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,5 @@
import ApiService from '../../api/ApiService';
const TypesApiService = new ApiService('types');
export default TypesApiService;

View File

@ -0,0 +1,43 @@
import PropTypes from 'prop-types';
import { Button, Form } from 'react-bootstrap';
import { useNavigate } from 'react-router-dom';
import TypesItemForm from './TypesItemForm.jsx';
import useTypesItemForm from '../types/hooks/TypesItemFormHook';
const TypesForm = ({ id }) => {
const navigate = useNavigate();
const {
type,
validated,
handleSubmit,
handleChange,
} = useTypesItemForm(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}>
<TypesItemForm item={type} handleChange={handleChange} />
<Button type='submit' variant='primary' onClick={() => onSubmit()}>
Сохранить
</Button>
</Form>
</>
);
};
TypesForm.propTypes = {
id: PropTypes.string,
};
export default TypesForm;

View File

@ -0,0 +1,19 @@
import PropTypes from 'prop-types';
import Input from '../input/Input.jsx';
const TypesItemForm = ({ item, handleChange }) => {
return (
<>
<Input name='name' label='Имя' value={item.name} type='text' onChange={handleChange} required />
<Input name='mail' label='Почта' value={item.mail} type='mail' onChange={handleChange} required />
<Input name='password' label='Пароль' value={item.password} onChange={handleChange} type='password' required />
</>
);
};
TypesItemForm.propTypes = {
item: PropTypes.object,
handleChange: PropTypes.func,
};
export default TypesItemForm;

0
src/index.css Normal file
View File

79
src/main.jsx Normal file
View File

@ -0,0 +1,79 @@
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 Login from './pages/Login.jsx';
import Register from './pages/Register.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';
import Redact from './pages/Redact.jsx';
const routes = [
{
index: true,
path: '/Main',
element: <MainPage />,
title: 'Main',
},
{
path: '/Profile',
element: <ProfilePage />,
title: 'Profile',
},
{
path: '/login',
element: <Login />,
title: 'Вход',
},
{
path: '/regist',
element: <Register />,
},
{
path: '/Book',
element: <BookPage />,
},
{
path: '/Author/:id?',
element: <AuthorPage />,
},
{
path: '/TextPage/:id?',
element: <TextPage />,
},
{
path: '/Admin',
element: <AdminPage />,
title: 'Админка',
},
{
path: '/redact/:id?',
element: <Redact />,
},
{
path: '/BookPage/:id?',
element: <BookPage />,
},
];
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>,
);

12
src/pages/AdminPage.jsx Normal file
View File

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

93
src/pages/AuthorPage.jsx Normal file
View File

@ -0,0 +1,93 @@
import { Link, useParams } from 'react-router-dom';
import ava from '../../images/Tolstoy.png';
import book from '../../images/book.png';
const AuthorPage = () => {
const { id } = useParams();
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={ava} 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>Имя автора</h2>
</div>
<div className="Window rounded-4 border border-4 border-dark p-2 mx-3">
<h3>
биография биография биография биография биография
биография биография биография биография биография
биография биография биография биография биография
биография биография биография биография биография
биография биография биография биография биография
биография биография биография биография биография
биография биография биография биография биография
биография биография биография биография биография
</h3>
</div>
</div>
</div>
<div className="bookPanel row mx-4 pt-4 border border-5 border-dark rounded-5 mb-5">
<Link className="a-main col-lg-2 col-md-4 col-sm-6 text-center" to="/BookPage">
<div className="text-center border border-4 border-dark">
<img src={book} className="img-fluid" alt="Responsive image"></img>
</div>
<h3>Название</h3>
<h3>Автор</h3>
</Link>
<Link className="a-main col-lg-2 col-md-4 col-sm-6 text-center" to="/BookPage">
<div className="text-center border border-4 border-dark">
<img src={book} className="img-fluid" alt="Responsive image"></img>
</div>
<h3>Название</h3>
<h3>Author</h3>
</Link>
<Link className="a-main col-lg-2 col-md-4 col-sm-6 text-center" to="/BookPage">
<div className="text-center border border-4 border-dark">
<img src={book} className="img-fluid" alt="Responsive image"></img>
</div>
<h3>Название</h3>
<h3>Author</h3>
</Link>
<Link className="a-main col-lg-2 col-md-4 col-sm-6 text-center" to="/BookPage">
<div className="text-center border border-4 border-dark">
<img src={book} className="img-fluid" alt="Responsive image"></img>
</div>
<h3>Название</h3>
<h3>Author</h3>
</Link>
<Link className="a-main col-lg-2 col-md-4 col-sm-6 text-center" to="/BookPage">
<div className="text-center border border-4 border-dark">
<img src={book} className="img-fluid" alt="Responsive image"></img>
</div>
<h3>Название</h3>
<h3>Author</h3>
</Link>
<Link className="a-main col-lg-2 col-md-4 col-sm-6 text-center" to="/BookPage">
<div className="text-center border border-4 border-dark">
<img src={book} className="img-fluid" alt="Responsive image"></img>
</div>
<h3>Название</h3>
<h3>Автор</h3>
</Link>
</div>
</main>
</>
);
};
export default AuthorPage;

53
src/pages/BookPage.jsx Normal file
View File

@ -0,0 +1,53 @@
import { Link, useParams } from 'react-router-dom';
import useLinesItemForm from '../components/lines/hooks/LinesItemFormHook';
import useType from '../components/types/hooks/TypeHook';
const BookPage = () => {
const { id } = useParams();
const {
item,
} = useLinesItemForm(id);
const { type } = useType(id);
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/${item.id}`}>
<button className="border border-dark border-5 rounded-4 col-12">
<h3 className="mx-3">
Читать
</h3>
</button>
</Link>
</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={`/Author/${type.id}`}>{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;

19
src/pages/ErrorPage.jsx Normal file
View File

@ -0,0 +1,19 @@
import { Alert, Button, 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">
Страница не найдена
</Alert>
<Button className="w-25 mt-2" variant="primary" onClick={() => navigate(-1)}>Назад</Button>
</Container>
</Container>
);
};
export default ErrorPage;

74
src/pages/Login.jsx Normal file
View File

@ -0,0 +1,74 @@
import { useNavigate } from 'react-router-dom';
import { useState } from 'react';
import { Button, Form } from 'react-bootstrap';
import usePersonL from '../components/types/hooks/LoginHooks';
const Login = () => {
const navigator = useNavigate();
const RedirectRegist = () => {
navigator('/regist');
};
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) {
return;
}
if (user.password === formData.password) {
const ind = user.id;
localStorage.setItem('EnabledUser', JSON.stringify(user.id));
navigator(`/Author/${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 (
<>
<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>E-mail</Form.Label>
<Form.Control type='email' name='email' placeholder='name@example.ru' required
value={formData.email} onChange={handleChange} />
</Form.Group>
<Form.Group className='mb-2' controlId='password'>
<Form.Label>Пароль</Form.Label>
<Form.Control type='password' name='password' required
value={formData.password} onChange={handleChange} />
</Form.Group>
<div className='text-center'>
<Button className='w-50' variant='primary'>Войти</Button>
</div>
<div className='text-center'>
<Button className='w-50' variant='primary' onClick={RedirectRegist} >Регистрация</Button>
</div>
</Form>
</>
);
};
export default Login;

87
src/pages/LoginPage.jsx Normal file
View File

@ -0,0 +1,87 @@
// import { Link } from 'react-router-dom';
import { Form } from 'react-bootstrap';
const LoginPage = () => {
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">
Регистрация
</h1>
<div className="mb-3">
<label className="form-label">
<b>username:</b>
</label>
<Form.Control className=" form-control border border-4 border-dark rounded-4" type="text" required></Form.Control>
</div>
<div className="mb-3">
<label className="form-label">
<b>email:</b>
</label>
<Form.Control className=" form-control border border-4 border-dark rounded-4" type="text" required></Form.Control>
</div>
<div className="mb-3">
<label className="form-label">
<b>password:</b>
</label>
<Form.Control className=" form-control border border-4 border-dark rounded-4" type="text" required></Form.Control>
</div>
<div className="mb-3">
<label className="form-label">
<b>password confirm:</b>
</label>
<Form.Control className=" form-control border border-4 border-dark rounded-4" type="text" required></Form.Control>
</div>
<div className="col">
<button className="border border-dark border-5 rounded-4 col-12">
<h3 className="mx-3">
Регистрация
</h3>
</button>
</div>
</div>
<div className="col-sm">
<h1 className="text-center">
Вход
</h1>
<div className="mb-3">
<label className="form-label">
<b>email:</b>
</label>
<Form.Control className=" form-control border border-4 border-dark rounded-4" type="text" required></Form.Control>
</div>
<div className="mb-3">
<label className="form-label">
<b>password:</b>
</label>
<Form.Control className=" form-control border border-4 border-dark rounded-4" type="text" required></Form.Control>
</div>
<div className="col">
<button className=" border border-dark border-5 rounded-4 col-12">
<h3 className="mx-3">
Вход
</h3>
</button>
</div>
</div>
</div>
</main>
</>
);
};
export default LoginPage;

37
src/pages/MainPage.jsx Normal file
View File

@ -0,0 +1,37 @@
import Button from '../components/Button/Button.jsx';
import useLines from '../components/lines/hooks/LinesHook';
const MainPage = () => {
const { lines } = useLines();
return (
<>
<>
<main className="mb-5">
<h3 id="news" className="section-name text-center col-xs-9 col-sm-6 col-mb-1 col-lg-2 justify-content-start mb-0 border border-bottom-0 border-3 border-dark">
Новое:
</h3>
<div className="window row mx-4 pt-4 border border-5 border-dark rounded-5">
{
lines.map((line) =>
<Button key={line.id} line={line} />)
}
</div>
<h3 id="popular" className="section-name text-center col-xs-9 col-sm-6 col-mb-1 col-lg-2 justify-content-start mb-0 border border-bottom-0 border-3 border-dark">
Популярное:
</h3>
<div className="window row mx-4 pt-4 border border-5 border-dark rounded-5">
{
lines.map((line) =>
<Button key={line.id} line={line} />)
}
</div>
</main>
</>
</>
);
};
export default MainPage;

91
src/pages/ProfilePage.jsx Normal file
View File

@ -0,0 +1,91 @@
import { Link } from 'react-router-dom';
const ProfilePage = () => {
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="images/woman.png" className="img-fluid border border-5 border-dark"></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">
Имя пользователя
</h1>
</div>
</div>
</div>
<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>Аннотация Аннотация Аннотация Аннотация Аннотация Аннотация
Аннотация Аннотация Аннотация Аннотация Аннотация Аннотация
Аннотация Аннотация Аннотация Аннотация Аннотация Аннотация
Аннотация Аннотация Аннотация Аннотация Аннотация Аннотация
Аннотация Аннотация Аннотация Аннотация Аннотация Аннотация
Аннотация Аннотация Аннотация Аннотация Аннотация Аннотация
Аннотация Аннотация Аннотация Аннотация Аннотация Аннотация
Аннотация Аннотация Аннотация Аннотация Аннотация Аннотация
Аннотация Аннотация Аннотация Аннотация Аннотация Аннотация
Аннотация Аннотация Аннотация Аннотация Аннотация Аннотация
</h3>
</div>
</div>
</div>
<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>Аннотация Аннотация Аннотация Аннотация Аннотация Аннотация
Аннотация Аннотация Аннотация Аннотация Аннотация Аннотация
Аннотация Аннотация Аннотация Аннотация Аннотация Аннотация
Аннотация Аннотация Аннотация Аннотация Аннотация Аннотация
Аннотация Аннотация Аннотация Аннотация Аннотация Аннотация
Аннотация Аннотация Аннотация Аннотация Аннотация Аннотация
Аннотация Аннотация Аннотация Аннотация Аннотация Аннотация
Аннотация Аннотация Аннотация Аннотация Аннотация Аннотация
Аннотация Аннотация Аннотация Аннотация Аннотация Аннотация
Аннотация Аннотация Аннотация Аннотация Аннотация Аннотация
</h3>
</div>
</div>
</div>
</main>
</>
);
};
export default ProfilePage;

11
src/pages/Redact.jsx Normal file
View File

@ -0,0 +1,11 @@
import { useParams } from 'react-router-dom';
import LinesForm from '../components/lines/form/LinesForm.jsx';
const Redact = () => {
const { id } = useParams();
return (
<LinesForm id={id} />
);
};
export default Redact;

14
src/pages/Register.jsx Normal file
View File

@ -0,0 +1,14 @@
import { useParams } from 'react-router-dom';
import TypesForm from '../components/user/TypesForm.jsx';
const Register = () => {
const { id } = useParams();
return (
<>
<TypesForm id={id} />
</>
);
};
export default Register;

29
src/pages/TextPage.jsx Normal file
View File

@ -0,0 +1,29 @@
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.title}</Link>
</h1>
</div>
</div>
<div className="Window m-5 border border-5 border-dark rounded-4 p-4">
<h2 className="book-text">
{item.video}
</h2>
</div>
</main>
</>
);
};
export default TextPage;

13
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,
},
});