Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d7f289234c | |||
| 294699289c | |||
| 10ddff97d0 | |||
| f347eb098b | |||
| e378591189 | |||
| 546f09fda3 | |||
| 51e33fd62d | |||
| ba1ad50382 | |||
| cc60f5808a | |||
| da02613094 | |||
| 07a092624b | |||
| 33f1f364b5 | |||
| f6de886c74 | |||
| 6006095ad7 | |||
| 077e8e59fc | |||
| fe9c162596 |
83
.idea/workspace.xml
generated
Normal file
@@ -0,0 +1,83 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AutoImportSettings">
|
||||
<option name="autoReloadType" value="SELECTIVE" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="857c5166-c43c-4106-983f-6753255d86a7" name="Changes" comment="">
|
||||
<change beforePath="$PROJECT_DIR$/online-library/package-lock.json" beforeDir="false" afterPath="$PROJECT_DIR$/online-library/package-lock.json" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/online-library/package.json" beforeDir="false" afterPath="$PROJECT_DIR$/online-library/package.json" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/online-library/src/BookCard/BookCard.jsx" beforeDir="false" afterPath="$PROJECT_DIR$/online-library/src/BookCard/BookCard.jsx" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/online-library/src/BookCardInfo/BookCardIInfo.jsx" beforeDir="false" afterPath="$PROJECT_DIR$/online-library/src/BookCardInfo/BookCardIInfo.jsx" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/online-library/src/Main/Main.jsx" beforeDir="false" afterPath="$PROJECT_DIR$/online-library/src/Main/Main.jsx" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||
</component>
|
||||
<component name="Git.Settings">
|
||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||
</component>
|
||||
<component name="ProjectColorInfo">{
|
||||
"customColor": "",
|
||||
"associatedIndex": 5
|
||||
}</component>
|
||||
<component name="ProjectId" id="32ugu54CHfH6I1mAvywPuSyNpUc" />
|
||||
<component name="ProjectViewState">
|
||||
<option name="hideEmptyMiddlePackages" value="true" />
|
||||
<option name="showLibraryContents" value="true" />
|
||||
</component>
|
||||
<component name="PropertiesComponent">{
|
||||
"keyToString": {
|
||||
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"git-widget-placeholder": "lab__work4",
|
||||
"node.js.detected.package.eslint": "true",
|
||||
"node.js.detected.package.tslint": "true",
|
||||
"node.js.selected.package.eslint": "(autodetect)",
|
||||
"node.js.selected.package.tslint": "(autodetect)",
|
||||
"nodejs_package_manager_path": "npm",
|
||||
"npm.start.executor": "Run",
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
}
|
||||
}</component>
|
||||
<component name="RunManager">
|
||||
<configuration name="start" type="js.build_tools.npm" nameIsGenerated="true">
|
||||
<package-json value="$PROJECT_DIR$/online-library/package.json" />
|
||||
<command value="run" />
|
||||
<scripts>
|
||||
<script value="start" />
|
||||
</scripts>
|
||||
<node-interpreter value="project" />
|
||||
<envs />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
<component name="SharedIndexes">
|
||||
<attachedChunks>
|
||||
<set>
|
||||
<option value="bundled-jdk-9823dce3aa75-fbdcb00ec9e3-intellij.indexing.shared.core-IU-251.23774.435" />
|
||||
<option value="bundled-js-predefined-d6986cc7102b-f27c65a3e318-JavaScript-IU-251.23774.435" />
|
||||
</set>
|
||||
</attachedChunks>
|
||||
</component>
|
||||
<component name="TaskManager">
|
||||
<task active="true" id="Default" summary="Default task">
|
||||
<changelist id="857c5166-c43c-4106-983f-6753255d86a7" name="Changes" comment="" />
|
||||
<created>1758276091066</created>
|
||||
<option name="number" value="Default" />
|
||||
<option name="presentableId" value="Default" />
|
||||
<updated>1758276091066</updated>
|
||||
<workItem from="1758276092290" duration="2000" />
|
||||
<workItem from="1758276100014" duration="1788000" />
|
||||
<workItem from="1758286909451" duration="8000" />
|
||||
<workItem from="1758286999127" duration="5000" />
|
||||
</task>
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
<option name="version" value="3" />
|
||||
</component>
|
||||
</project>
|
||||
22
database/db.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"books":[
|
||||
{
|
||||
"id":1,
|
||||
"title":"title",
|
||||
"author":"author",
|
||||
"annotation":"rtesfdsdfx",
|
||||
"link_img":"/res/1984.png"
|
||||
}
|
||||
|
||||
],
|
||||
"users":[
|
||||
{
|
||||
"id":1,
|
||||
"name":"name",
|
||||
"fullname":"fullname",
|
||||
"email":"email",
|
||||
"city":"city",
|
||||
"phone_number":1654113
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Книги по тематикам</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -20,19 +20,19 @@
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav mx-auto gap-3">
|
||||
<li class="nav-item"><a href="index.html" class="nav-link nav-link-custom">Главная страница</a></li>
|
||||
<li class="nav-item"><a href="search.html" class="nav-link nav-link-custom">Поиск</a></li>
|
||||
<li class="nav-item"><a href="/html/index.html" class="nav-link nav-link-custom">Главная страница</a></li>
|
||||
<li class="nav-item"><a href="/html/search.html" class="nav-link nav-link-custom">Поиск</a></li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link nav-link-custom dropdown-toggle" href="choice.html" role="button"
|
||||
<a class="nav-link nav-link-custom dropdown-toggle" href="/html/choice.html" role="button"
|
||||
data-bs-toggle="dropdown">Что выбрать</a>
|
||||
<ul class="dropdown-menu dropdown-menu-dark">
|
||||
<li><a class="dropdown-item" href="choice.html#военные">Военные книги</a></li>
|
||||
<li><a class="dropdown-item" href="choice.html#наука">Научная литература</a></li>
|
||||
<li><a class="dropdown-item" href="choice.html#дети">Детские книги</a></li>
|
||||
<li><a class="dropdown-item" href="choice.html#психология">Психология</a></li>
|
||||
<li><a class="dropdown-item" href="/html/choice.html#военные">Военные книги</a></li>
|
||||
<li><a class="dropdown-item" href="/html/choice.html#наука">Научная литература</a></li>
|
||||
<li><a class="dropdown-item" href="/html/choice.html#дети">Детские книги</a></li>
|
||||
<li><a class="dropdown-item" href="/html/choice.html#психология">Психология</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item"><a href="lk.html" class="nav-link nav-link-custom">Личный кабинет</a></li>
|
||||
<li class="nav-item"><a href="/html/lk.html" class="nav-link nav-link-custom">Личный кабинет</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -49,15 +49,15 @@
|
||||
<div class="list-group">
|
||||
<div class="book-item list-group-item d-flex justify-content-between align-items-center mb-2">
|
||||
<span>Эрих Мария Ремарк "На Западном фронте без перемен"</span>
|
||||
<a href="infobook.html" class="btn btn-sm" style="background-color: var(--accent); color: var(--bg-dark)">Читать</a>
|
||||
<a href="/html/infobook.html" class="btn btn-sm" style="background-color: var(--accent); color: var(--bg-dark)">Читать</a>
|
||||
</div>
|
||||
<div class="book-item list-group-item d-flex justify-content-between align-items-center mb-2">
|
||||
<span>Лев Толстой "Война и мир"</span>
|
||||
<a href="infobook.html" class="btn btn-sm" style="background-color: var(--accent); color: var(--bg-dark)">Читать</a>
|
||||
<a href="/html/infobook.html" class="btn btn-sm" style="background-color: var(--accent); color: var(--bg-dark)">Читать</a>
|
||||
</div>
|
||||
<div class="book-item list-group-item d-flex justify-content-between align-items-center mb-2">
|
||||
<span>Василь Быков "Сотников"</span>
|
||||
<a href="infobook.html" class="btn btn-sm" style="background-color: var(--accent); color: var(--bg-dark)">Читать</a>
|
||||
<a href="/html/infobook.html" class="btn btn-sm" style="background-color: var(--accent); color: var(--bg-dark)">Читать</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -68,15 +68,15 @@
|
||||
<div class="list-group">
|
||||
<div class="book-item list-group-item d-flex justify-content-between align-items-center mb-2">
|
||||
<span>Стивен Хокинг "Краткая история времени"</span>
|
||||
<a href="infobook.html" class="btn btn-sm" style="background-color: var(--accent); color: var(--bg-dark)">Читать</a>
|
||||
<a href="/html/infobook.html" class="btn btn-sm" style="background-color: var(--accent); color: var(--bg-dark)">Читать</a>
|
||||
</div>
|
||||
<div class="book-item list-group-item d-flex justify-content-between align-items-center mb-2">
|
||||
<span>Карл Саган "Космос"</span>
|
||||
<a href="infobook.html" class="btn btn-sm" style="background-color: var(--accent); color: var(--bg-dark)">Читать</a>
|
||||
<a href="/html/infobook.html" class="btn btn-sm" style="background-color: var(--accent); color: var(--bg-dark)">Читать</a>
|
||||
</div>
|
||||
<div class="book-item list-group-item d-flex justify-content-between align-items-center mb-2">
|
||||
<span>Ричард Докинз "Эгоистичный ген"</span>
|
||||
<a href="infobook.html" class="btn btn-sm" style="background-color: var(--accent); color: var(--bg-dark)">Читать</a>
|
||||
<a href="/html/infobook.html" class="btn btn-sm" style="background-color: var(--accent); color: var(--bg-dark)">Читать</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -87,15 +87,15 @@
|
||||
<div class="list-group">
|
||||
<div class="book-item list-group-item d-flex justify-content-between align-items-center mb-2">
|
||||
<span>Астрид Линдгрен "Пеппи Длинныйчулок"</span>
|
||||
<a href="infobook.html" class="btn btn-sm" style="background-color: var(--accent); color: var(--bg-dark)">Читать</a>
|
||||
<a href="/html/infobook.html" class="btn btn-sm" style="background-color: var(--accent); color: var(--bg-dark)">Читать</a>
|
||||
</div>
|
||||
<div class="book-item list-group-item d-flex justify-content-between align-items-center mb-2">
|
||||
<span>Алан Милн "Винни-Пух"</span>
|
||||
<a href="infobook.html" class="btn btn-sm" style="background-color: var(--accent); color: var(--bg-dark)">Читать</a>
|
||||
<a href="/html/infobook.html" class="btn btn-sm" style="background-color: var(--accent); color: var(--bg-dark)">Читать</a>
|
||||
</div>
|
||||
<div class="book-item list-group-item d-flex justify-content-between align-items-center mb-2">
|
||||
<span>Антуан де Сент-Экзюпери "Маленький принц"</span>
|
||||
<a href="infobook.html" class="btn btn-sm" style="background-color: var(--accent); color: var(--bg-dark)">Читать</a>
|
||||
<a href="/html/infobook.html" class="btn btn-sm" style="background-color: var(--accent); color: var(--bg-dark)">Читать</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -106,15 +106,15 @@
|
||||
<div class="list-group">
|
||||
<div class="book-item list-group-item d-flex justify-content-between align-items-center mb-2">
|
||||
<span>"Как завоевывать друзей и оказывать влияние на людей"</span>
|
||||
<a href="infobook.html" class="btn btn-sm" style="background-color: var(--accent); color: var(--bg-dark)">Читать</a>
|
||||
<a href="/html/infobook.html" class="btn btn-sm" style="background-color: var(--accent); color: var(--bg-dark)">Читать</a>
|
||||
</div>
|
||||
<div class="book-item list-group-item d-flex justify-content-between align-items-center mb-2">
|
||||
<span>Виктор Франкл "Человек в поисках смысла"</span>
|
||||
<a href="infobook.html" class="btn btn-sm" style="background-color: var(--accent); color: var(--bg-dark)">Читать</a>
|
||||
<a href="/html/infobook.html" class="btn btn-sm" style="background-color: var(--accent); color: var(--bg-dark)">Читать</a>
|
||||
</div>
|
||||
<div class="book-item list-group-item d-flex justify-content-between align-items-center mb-2">
|
||||
<span>Роберт Чалдини "Психология влияния"</span>
|
||||
<a href="infobook.html" class="btn btn-sm" style="background-color: var(--accent); color: var(--bg-dark)">Читать</a>
|
||||
<a href="/html/infobook.html" class="btn btn-sm" style="background-color: var(--accent); color: var(--bg-dark)">Читать</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -132,8 +132,8 @@
|
||||
<div class="col-md-6">
|
||||
<h3>Соцсети</h3>
|
||||
<div class="d-flex justify-content-center gap-3">
|
||||
<a href="https://vk.ru"><img src="vk.svg" alt="VK" style="width: 32px"></a>
|
||||
<a href="https://lichess.org"><img src="telegram.svg" alt="Telegram" style="width: 32px"></a>
|
||||
<a href="https://vk.ru"><img src="/res/vk.svg" alt="VK" style="width: 32px"></a>
|
||||
<a href="https://lichess.org"><img src="/res/telegram.svg" alt="Telegram" style="width: 32px"></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Главная страница</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
</head>
|
||||
|
||||
<body class="p-4">
|
||||
@@ -20,19 +20,19 @@
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav mx-auto gap-3">
|
||||
<li class="nav-item"><a href="index.html" class="nav-link nav-link-custom">Главная страница</a></li>
|
||||
<li class="nav-item"><a href="search.html" class="nav-link nav-link-custom">Поиск</a></li>
|
||||
<li class="nav-item"><a href="/html/index.html" class="nav-link nav-link-custom">Главная страница</a></li>
|
||||
<li class="nav-item"><a href="/html/search.html" class="nav-link nav-link-custom">Поиск</a></li>
|
||||
<li class="nav-item dropdown dropdown-custom">
|
||||
<a class="nav-link nav-link-custom dropdown-toggle" href="choice.html" role="button"
|
||||
<a class="nav-link nav-link-custom dropdown-toggle" href="/html/choice.html" role="button"
|
||||
data-bs-toggle="dropdown">Что выбрать</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="choice.html#военные">Военные книги</a></li>
|
||||
<li><a class="dropdown-item" href="choice.html#наука">Научная литература</a></li>
|
||||
<li><a class="dropdown-item" href="choice.html#дети">Детские книги</a></li>
|
||||
<li><a class="dropdown-item" href="choice.html#психология">Психология</a></li>
|
||||
<li><a class="dropdown-item" href="/html/choice.html#военные">Военные книги</a></li>
|
||||
<li><a class="dropdown-item" href="/html/choice.html#наука">Научная литература</a></li>
|
||||
<li><a class="dropdown-item" href="/html/choice.html#дети">Детские книги</a></li>
|
||||
<li><a class="dropdown-item" href="/html/choice.html#психология">Психология</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item"><a href="lk.html" class="nav-link nav-link-custom">Личный кабинет</a></li>
|
||||
<li class="nav-item"><a href="/html/lk.html" class="nav-link nav-link-custom">Личный кабинет</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -44,41 +44,41 @@
|
||||
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
|
||||
<div class="col">
|
||||
<div class="book-card p-3 h-100 text-center">
|
||||
<img src="1984.jpg" class="img-fluid rounded mb-3" alt="Обложка книги">
|
||||
<img src="/res/1984.jpg" class="img-fluid rounded mb-3" alt="Обложка книги">
|
||||
<p class="fs-5">Джордж Оруэлл "1984"<br>
|
||||
<a href="infobook.html" class="text-decoration-none" style="color: var(--accent)">Ссылка на переход</a>
|
||||
<a href="/html/infobook.html" class="text-decoration-none" style="color: var(--accent)">Ссылка на переход</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="book-card p-3 h-100 text-center">
|
||||
<img src="мы.jpg" class="img-fluid rounded mb-3" alt="Обложка книги">
|
||||
<img src="/res/мы.jpg" class="img-fluid rounded mb-3" alt="Обложка книги">
|
||||
<p class="fs-5">Замятин "Мы"<br>
|
||||
<a href="infobook.html" class="text-decoration-none" style="color: var(--accent)">Ссылка на переход</a>
|
||||
<a href="/html/infobook.html" class="text-decoration-none" style="color: var(--accent)">Ссылка на переход</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="book-card p-3 h-100 text-center">
|
||||
<img src="451 по фарингейту.jpg" class="img-fluid rounded mb-3" alt="Обложка книги">
|
||||
<img src="/res/451 по фарингейту.jpg" class="img-fluid rounded mb-3" alt="Обложка книги">
|
||||
<p class="fs-5">Рэй Брэдбери "451 по Фаренгейту"<br>
|
||||
<a href="infobook.html" class="text-decoration-none" style="color: var(--accent)">Ссылка на переход</a>
|
||||
<a href="/html/infobook.html" class="text-decoration-none" style="color: var(--accent)">Ссылка на переход</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="book-card p-3 h-100 text-center">
|
||||
<img src="о дивный новый мир.jpg" class="img-fluid rounded mb-3" alt="Обложка книги">
|
||||
<img src="/res/о дивный новый мир.jpg" class="img-fluid rounded mb-3" alt="Обложка книги">
|
||||
<p class="fs-5">Олдос Хаксли "О дивный новый мир"<br>
|
||||
<a href="infobook.html" class="text-decoration-none" style="color: var(--accent)">Ссылка на переход</a>
|
||||
<a href="/html/infobook.html" class="text-decoration-none" style="color: var(--accent)">Ссылка на переход</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="book-card p-3 h-100 text-center">
|
||||
<img src="повелитель мух .jpg" class="img-fluid rounded mb-3" alt="Обложка книги">
|
||||
<img src="/res/повелитель мух .jpg" class="img-fluid rounded mb-3" alt="Обложка книги">
|
||||
<p class="fs-5">Уильям Голдинг "Повелитель мух"<br>
|
||||
<a href="infobook.html" class="text-decoration-none" style="color: var(--accent)">Ссылка на переход</a>
|
||||
<a href="/html/infobook.html" class="text-decoration-none" style="color: var(--accent)">Ссылка на переход</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -96,8 +96,8 @@
|
||||
<div class="col-md-6">
|
||||
<h3>Соцсети</h3>
|
||||
<div class="d-flex justify-content-center gap-3">
|
||||
<a href="https://vk.ru"><img src="vk.svg" alt="VK" style="width: 32px"></a>
|
||||
<a href="https://lichess.org"><img src="telegram.svg" alt="Telegram" style="width: 32px"></a>
|
||||
<a href="https://vk.ru"><img src="/res/vk.svg" alt="VK" style="width: 32px"></a>
|
||||
<a href="https://lichess.org"><img src="/res/telegram.svg" alt="Telegram" style="width: 32px"></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Главная страница</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
</head>
|
||||
|
||||
<body class="p-4">
|
||||
@@ -20,19 +20,19 @@
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav mx-auto gap-3">
|
||||
<li class="nav-item"><a href="index.html" class="nav-link nav-link-custom">Главная страница</a></li>
|
||||
<li class="nav-item"><a href="search.html" class="nav-link nav-link-custom">Поиск</a></li>
|
||||
<li class="nav-item"><a href="/html/index.html" class="nav-link nav-link-custom">Главная страница</a></li>
|
||||
<li class="nav-item"><a href="/html/search.html" class="nav-link nav-link-custom">Поиск</a></li>
|
||||
<li class="nav-item dropdown dropdown-custom">
|
||||
<a class="nav-link nav-link-custom dropdown-toggle" href="choice.html" role="button"
|
||||
<a class="nav-link nav-link-custom dropdown-toggle" href="/html/choice.html" role="button"
|
||||
data-bs-toggle="dropdown">Что выбрать</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="choice.html#военные">Военные книги</a></li>
|
||||
<li><a class="dropdown-item" href="choice.html#наука">Научная литература</a></li>
|
||||
<li><a class="dropdown-item" href="choice.html#дети">Детские книги</a></li>
|
||||
<li><a class="dropdown-item" href="choice.html#психология">Психология</a></li>
|
||||
<li><a class="dropdown-item" href="/html/choice.html#военные">Военные книги</a></li>
|
||||
<li><a class="dropdown-item" href="/html/choice.html#наука">Научная литература</a></li>
|
||||
<li><a class="dropdown-item" href="/html/choice.html#дети">Детские книги</a></li>
|
||||
<li><a class="dropdown-item" href="/html/choice.html#психология">Психология</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item"><a href="lk.html" class="nav-link nav-link-custom">Личный кабинет</a></li>
|
||||
<li class="nav-item"><a href="/html/lk.html" class="nav-link nav-link-custom">Личный кабинет</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -47,7 +47,7 @@
|
||||
<h2 class="fs-1 mb-3">Джордж Оруэлл "1984"</h2>
|
||||
|
||||
<div class="text-center mb-4">
|
||||
<img src="1984.jpg" alt="Обложка книги 1984" class="img-fluid rounded" style="max-width: 300px">
|
||||
<img src="/res/1984.jpg" alt="Обложка книги 1984" class="img-fluid rounded" style="max-width: 300px">
|
||||
</div>
|
||||
|
||||
<div class="mb-4 p-3 --bg-medium rounded">
|
||||
@@ -90,8 +90,8 @@
|
||||
<div class="col-md-6">
|
||||
<h3>Соцсети</h3>
|
||||
<div class="d-flex justify-content-center gap-3">
|
||||
<a href="https://vk.ru"><img src="vk.svg" alt="VK" style="width: 32px"></a>
|
||||
<a href="https://lichess.org"><img src="telegram.svg" alt="Telegram" style="width: 32px"></a>
|
||||
<a href="https://vk.ru"><img src="/res/vk.svg" alt="VK" style="width: 32px"></a>
|
||||
<a href="https://lichess.org"><img src="/res/telegram.svg" alt="Telegram" style="width: 32px"></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Личный кабинет</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -20,19 +20,19 @@
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav mx-auto gap-3">
|
||||
<li class="nav-item"><a href="index.html" class="nav-link nav-link-custom">Главная страница</a></li>
|
||||
<li class="nav-item"><a href="search.html" class="nav-link nav-link-custom">Поиск</a></li>
|
||||
<li class="nav-item"><a href="/html/index.html" class="nav-link nav-link-custom">Главная страница</a></li>
|
||||
<li class="nav-item"><a href="/html/search.html" class="nav-link nav-link-custom">Поиск</a></li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link nav-link-custom dropdown-toggle" href="choice.html" role="button"
|
||||
<a class="nav-link nav-link-custom dropdown-toggle" href="/html/choice.html" role="button"
|
||||
data-bs-toggle="dropdown">Что выбрать</a>
|
||||
<ul class="dropdown-menu dropdown-menu-dark">
|
||||
<li><a class="dropdown-item" href="choice.html#военные">Военные книги</a></li>
|
||||
<li><a class="dropdown-item" href="choice.html#наука">Научная литература</a></li>
|
||||
<li><a class="dropdown-item" href="choice.html#дети">Детские книги</a></li>
|
||||
<li><a class="dropdown-item" href="choice.html#психология">Психология</a></li>
|
||||
<li><a class="dropdown-item" href="/html/choice.html#военные">Военные книги</a></li>
|
||||
<li><a class="dropdown-item" href="/html/choice.html#наука">Научная литература</a></li>
|
||||
<li><a class="dropdown-item" href="/html/choice.html#дети">Детские книги</a></li>
|
||||
<li><a class="dropdown-item" href="/html/choice.html#психология">Психология</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item"><a href="lk.html" class="nav-link nav-link-custom">Личный кабинет</a></li>
|
||||
<li class="nav-item"><a href="/html/lk.html" class="nav-link nav-link-custom">Личный кабинет</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -86,8 +86,8 @@
|
||||
<div class="col-md-6">
|
||||
<h3>Соцсети</h3>
|
||||
<div class="d-flex justify-content-center gap-3">
|
||||
<a href="https://vk.ru"><img src="vk.svg" alt="VK" style="width: 32px"></a>
|
||||
<a href="https://lichess.org"><img src="telegram.svg" alt="Telegram" style="width: 32px"></a>
|
||||
<a href="https://vk.ru"><img src="/res/vk.svg" alt="VK" style="width: 32px"></a>
|
||||
<a href="https://lichess.org"><img src="/res/telegram.svg" alt="Telegram" style="width: 32px"></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Поиск книг</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -20,19 +20,19 @@
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav mx-auto gap-3">
|
||||
<li class="nav-item"><a href="index.html" class="nav-link nav-link-custom">Главная страница</a></li>
|
||||
<li class="nav-item"><a href="search.html" class="nav-link nav-link-custom">Поиск</a></li>
|
||||
<li class="nav-item"><a href="/html/index.html" class="nav-link nav-link-custom">Главная страница</a></li>
|
||||
<li class="nav-item"><a href="/html/search.html" class="nav-link nav-link-custom">Поиск</a></li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link nav-link-custom dropdown-toggle" href="choice.html" role="button"
|
||||
<a class="nav-link nav-link-custom dropdown-toggle" href="/html/choice.html" role="button"
|
||||
data-bs-toggle="dropdown">Что выбрать</a>
|
||||
<ul class="dropdown-menu dropdown-menu-dark">
|
||||
<li><a class="dropdown-item" href="choice.html#военные">Военные книги</a></li>
|
||||
<li><a class="dropdown-item" href="choice.html#наука">Научная литература</a></li>
|
||||
<li><a class="dropdown-item" href="choice.html#дети">Детские книги</a></li>
|
||||
<li><a class="dropdown-item" href="choice.html#психология">Психология</a></li>
|
||||
<li><a class="dropdown-item" href="/html/choice.html#военные">Военные книги</a></li>
|
||||
<li><a class="dropdown-item" href="/html/choice.html#наука">Научная литература</a></li>
|
||||
<li><a class="dropdown-item" href="/html/choice.html#дети">Детские книги</a></li>
|
||||
<li><a class="dropdown-item" href="/html/choice.html#психология">Психология</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item"><a href="lk.html" class="nav-link nav-link-custom">Личный кабинет</a></li>
|
||||
<li class="nav-item"><a href="/html/lk.html" class="nav-link nav-link-custom">Личный кабинет</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -47,25 +47,25 @@
|
||||
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
|
||||
<div class="col">
|
||||
<div class="book-card p-3 h-100 text-center">
|
||||
<img src="1984.jpg" class="img-fluid rounded mb-3" alt="Обложка книги">
|
||||
<img src="/res/1984.jpg" class="img-fluid rounded mb-3" alt="Обложка книги">
|
||||
<p class="fs-5">Джордж Оруэлл "1984"<br>
|
||||
<a href="infobook.html" class="text-decoration-none" style="color: var(--accent)">Ссылка на переход</a>
|
||||
<a href="/html/infobook.html" class="text-decoration-none" style="color: var(--accent)">Ссылка на переход</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="book-card p-3 h-100 text-center">
|
||||
<img src="1984.jpg" class="img-fluid rounded mb-3" alt="Обложка книги">
|
||||
<img src="/res/1984.jpg" class="img-fluid rounded mb-3" alt="Обложка книги">
|
||||
<p class="fs-5">Михаил Булгаков "Мастер и Маргарита"<br>
|
||||
<a href="infobook.html" class="text-decoration-none" style="color: var(--accent)">Ссылка на переход</a>
|
||||
<a href="/html/infobook.html" class="text-decoration-none" style="color: var(--accent)">Ссылка на переход</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="book-card p-3 h-100 text-center">
|
||||
<img src="1984.jpg" class="img-fluid rounded mb-3" alt="Обложка книги">
|
||||
<img src="/res/1984.jpg" class="img-fluid rounded mb-3" alt="Обложка книги">
|
||||
<p class="fs-5">Федор Достоевский "Преступление и наказание"<br>
|
||||
<a href="infobook.html" class="text-decoration-none" style="color: var(--accent)">Ссылка на переход</a>
|
||||
<a href="/html/infobook.html" class="text-decoration-none" style="color: var(--accent)">Ссылка на переход</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -82,8 +82,8 @@
|
||||
<div class="col-md-6">
|
||||
<h3>Соцсети</h3>
|
||||
<div class="d-flex justify-content-center gap-3">
|
||||
<a href="https://vk.ru"><img src="vk.svg" alt="VK" style="width: 32px"></a>
|
||||
<a href="https://lichess.org"><img src="telegram.svg" alt="Telegram" style="width: 32px"></a>
|
||||
<a href="https://vk.ru"><img src="/res/vk.svg" alt="VK" style="width: 32px"></a>
|
||||
<a href="https://lichess.org"><img src="/res/telegram.svg" alt="Telegram" style="width: 32px"></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
24
node_modules/.package-lock.json
generated
vendored
@@ -2302,18 +2302,6 @@
|
||||
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/json5": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
|
||||
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.0"
|
||||
},
|
||||
"bin": {
|
||||
"json5": "lib/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
@@ -3674,6 +3662,18 @@
|
||||
"strip-bom": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tsconfig-paths/node_modules/json5": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
|
||||
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.0"
|
||||
},
|
||||
"bin": {
|
||||
"json5": "lib/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
|
||||
6
node_modules/.vite/deps/_metadata.json
generated
vendored
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"hash": "a4354f03",
|
||||
"hash": "f196cd23",
|
||||
"configHash": "f1521633",
|
||||
"lockfileHash": "35a58dc0",
|
||||
"browserHash": "41763e6c",
|
||||
"lockfileHash": "c6b67ffc",
|
||||
"browserHash": "fa914368",
|
||||
"optimized": {},
|
||||
"chunks": {}
|
||||
}
|
||||
0
node_modules/.bin/json5 → node_modules/tsconfig-paths/node_modules/.bin/json5
generated
vendored
23
online-library/.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
70
online-library/README.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# Getting Started with Create React App
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm start`
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
|
||||
|
||||
The page will reload when you make changes.\
|
||||
You may also see any lint errors in the console.
|
||||
|
||||
### `npm test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.\
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `npm run eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
|
||||
|
||||
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
|
||||
|
||||
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
|
||||
### Code Splitting
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
|
||||
|
||||
### Analyzing the Bundle Size
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
|
||||
|
||||
### Making a Progressive Web App
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
|
||||
|
||||
### Deployment
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
|
||||
|
||||
### `npm run build` fails to minify
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
|
||||
16937
online-library/package-lock.json
generated
Normal file
48
online-library/package.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "online-library",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@mui/material": "^7.3.2",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "^6.8.0",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"axios": "^1.12.2",
|
||||
"bootstrap": "^5.3.8",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"react": "^19.1.1",
|
||||
"react-bootstrap": "^2.10.10",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-router-dom": "^7.8.2",
|
||||
"react-router-hash-link": "^2.4.3",
|
||||
"react-scripts": "5.0.1",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
32
online-library/public/index.html
Normal file
@@ -0,0 +1,32 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
Before Width: | Height: | Size: 123 KiB After Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
154
online-library/src/ApiRequest/ApiClient.js
Normal file
@@ -0,0 +1,154 @@
|
||||
import axios from "axios";
|
||||
import {
|
||||
getAccessToken,
|
||||
isTokenExpired,
|
||||
removeAccessToken,
|
||||
setAccessToken,
|
||||
} from "./JwtUtils";
|
||||
|
||||
let isRefreshing = false;
|
||||
let failedQueue = [];
|
||||
const processQueue = (error, token = null) => {
|
||||
failedQueue.forEach((prom) => {
|
||||
if (error) {
|
||||
prom.reject(error);
|
||||
} else {
|
||||
prom.resolve(token);
|
||||
}
|
||||
});
|
||||
failedQueue = [];
|
||||
};
|
||||
|
||||
const ONLINE_LIBRARY_URL = "http://localhost:8080";
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: ONLINE_LIBRARY_URL + "/api/v1.0",
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const apiAuth = axios.create({
|
||||
baseURL: ONLINE_LIBRARY_URL,
|
||||
timeout: 10000,
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
api.interceptors.request.use(
|
||||
async (config) => {
|
||||
let token = getAccessToken();
|
||||
if (token) {
|
||||
if (isTokenExpired(token)) {
|
||||
if (!isRefreshing) {
|
||||
isRefreshing = true;
|
||||
try {
|
||||
const response = await apiAuth.post("/reload");
|
||||
const newToken = response.data;
|
||||
setAccessToken(newToken);
|
||||
token = newToken;
|
||||
processQueue(null, newToken);
|
||||
} catch (error) {
|
||||
processQueue(error, null);
|
||||
removeAccessToken();
|
||||
return Promise.reject(error);
|
||||
} finally {
|
||||
isRefreshing = false;
|
||||
}
|
||||
} else {
|
||||
return new Promise((resolve, reject) => {
|
||||
failedQueue.push({ resolve, reject });
|
||||
})
|
||||
.then((token) => {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
return config;
|
||||
})
|
||||
.catch((error) => Promise.reject(error));
|
||||
}
|
||||
}
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error) => {
|
||||
const originalRequest = error.config;
|
||||
|
||||
if (error.response?.status === 401 && !originalRequest._retry) {
|
||||
originalRequest._retry = true;
|
||||
|
||||
if (!isRefreshing) {
|
||||
isRefreshing = true;
|
||||
try {
|
||||
const response = await apiAuth.post("/reload");
|
||||
const newToken = response.data;
|
||||
setAccessToken(newToken);
|
||||
processQueue(null, newToken);
|
||||
originalRequest.headers.Authorization = `Bearer ${newToken}`;
|
||||
return api(originalRequest);
|
||||
} catch (error) {
|
||||
processQueue(error, null);
|
||||
removeAccessToken();
|
||||
return Promise.reject(error);
|
||||
} finally {
|
||||
isRefreshing = false;
|
||||
}
|
||||
} else {
|
||||
return new Promise((resolve, reject) => {
|
||||
failedQueue.push({ resolve, reject });
|
||||
})
|
||||
.then((token) => {
|
||||
originalRequest.headers.Authorization = `Bearer ${token}`;
|
||||
return api(originalRequest);
|
||||
})
|
||||
.catch((error) => Promise.reject(error));
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export const bookAPI = {
|
||||
getAll: (params = {}) => {
|
||||
return api.get("/books", { params });
|
||||
},
|
||||
getBookById: (id) => {
|
||||
return api.get(`/book/${id}`);
|
||||
},
|
||||
getBookByTitle: (title) => {
|
||||
return api.get("/book", { params: { title } });
|
||||
},
|
||||
saveBook: (book) => {
|
||||
return api.post("/book", book);
|
||||
},
|
||||
updateBook: (book) => {
|
||||
return api.put("/book", book);
|
||||
},
|
||||
deelteBook: (id) => {
|
||||
return api.delete("/book", { params: { id } });
|
||||
},
|
||||
getBooksGenre: (params = {}) => {
|
||||
return api.get("/genre", { params });
|
||||
},
|
||||
};
|
||||
|
||||
export const authAPI = {
|
||||
login: (user, params = {}) => {
|
||||
return apiAuth.post("/login", user, { params });
|
||||
},
|
||||
registration: (user, params = {}) => {
|
||||
return apiAuth.post("/registration", user, { params });
|
||||
},
|
||||
logout: (params = {}) => {
|
||||
return apiAuth.get("/logout", { params });
|
||||
},
|
||||
};
|
||||
18
online-library/src/ApiRequest/JwtUtils.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import {jwtDecode} from "jwt-decode";
|
||||
|
||||
export const getAccessToken = () => localStorage.getItem("access_token");
|
||||
export const setAccessToken = (token) =>
|
||||
localStorage.setItem("access_token", token);
|
||||
export const removeAccessToken = () => localStorage.removeItem("access_token");
|
||||
|
||||
export const isTokenExpired = (token) => {
|
||||
if (!token) return true;
|
||||
try {
|
||||
const decodedToken = jwtDecode(token);
|
||||
const currentTime = Date.now() / 1000;
|
||||
return decodedToken.exp < currentTime;
|
||||
} catch (error) {
|
||||
console.error("Error decoding token:", error);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
32
online-library/src/App.jsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import Headers from "./header/Headers";
|
||||
import Footer from "./Footer/Footer";
|
||||
import Main from "./Main/Main";
|
||||
import Search from "./Search/Search";
|
||||
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
||||
import Lk from "./Lk/Lk";
|
||||
import Choice from "./Choice/Choice";
|
||||
import BookCardInfo from "./BookCardInfo/BookCardIInfo";
|
||||
import AuthPages from "./Registration/AuthPages";
|
||||
import "./style.css";
|
||||
import "bootstrap/dist/css/bootstrap.css";
|
||||
import "bootstrap/dist/js/bootstrap.bundle.min";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<>
|
||||
<BrowserRouter>
|
||||
<Headers></Headers>
|
||||
<Routes>
|
||||
<Route path="/" element={<Main />} />
|
||||
<Route path="/search" element={<Search />}></Route>
|
||||
<Route path="/choice" element={<Choice />}></Route>
|
||||
<Route path="/lk" element={<Lk />}></Route>
|
||||
<Route path="/infobook/:id" element={<BookCardInfo />}></Route>
|
||||
<Route path="/registration" element={<AuthPages></AuthPages>}></Route>
|
||||
</Routes>
|
||||
<Footer></Footer>
|
||||
</BrowserRouter>
|
||||
</>
|
||||
);
|
||||
}
|
||||
export default App;
|
||||
5
online-library/src/BookCard/BookCard.css
Normal file
@@ -0,0 +1,5 @@
|
||||
.book-card {
|
||||
background-color: var(--bg-medium);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
30
online-library/src/BookCard/BookCard.jsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import "./BookCard.css"
|
||||
|
||||
function BookCard({book}) {
|
||||
console.log("BookCard - " + book)
|
||||
return (
|
||||
<div className="col">
|
||||
<div className="book-card p-3 h-100 text-center">
|
||||
<img
|
||||
src={`${book.linkOnImage}`}
|
||||
className="img-fluid rounded mb-3"
|
||||
alt="Обложка книги"
|
||||
/>
|
||||
<p className="fs-5">
|
||||
{book.title}
|
||||
<br />
|
||||
<Link
|
||||
to={`/infobook/${book.id}`}
|
||||
className="text-decoration-none"
|
||||
style={{ color: "var(--accent)" }}
|
||||
>
|
||||
Ссылка на переход
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default BookCard;
|
||||
116
online-library/src/BookCardInfo/BookCardIInfo.jsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import { useLocation, useParams } from "react-router-dom";
|
||||
import { useEffect, useState } from "react";
|
||||
import { bookAPI } from "../ApiRequest/ApiClient";
|
||||
|
||||
function BookCardInfo() {
|
||||
const { id } = useParams();
|
||||
const [book, setBook] = useState();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
useEffect(() => {
|
||||
const fetchBooks = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await bookAPI.getBookById(id);
|
||||
console.log(response.data);
|
||||
setBook(response.data);
|
||||
} catch (error) {
|
||||
setError(error.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchBooks();
|
||||
}, [id]);
|
||||
console.log(id);
|
||||
const testObject = {
|
||||
title: "Тестовая книга",
|
||||
description: "Описание книги",
|
||||
annotation: "Аннотация книги",
|
||||
author: "Автор",
|
||||
genre: "Жанр",
|
||||
year: "1949",
|
||||
};
|
||||
if (loading) return <div> Загрузка</div>;
|
||||
if (error) return <div> Ошибка: {error}</div>;
|
||||
if (!book) return <div> Книга не найдена</div>;
|
||||
return (
|
||||
<body className="bg-dark text-light p-4">
|
||||
{console.log(book.title)}
|
||||
<div className="container">
|
||||
<div
|
||||
className="card bg-secondary border-0 shadow-lg mb-4"
|
||||
style={{ maxWidth: "800px", margin: "0 auto" }}
|
||||
>
|
||||
<div className="card-body">
|
||||
<h1 className="text-center mb-4" style={{ color: "#00adb5" }}>
|
||||
{book.title}
|
||||
</h1>
|
||||
<h2 className="fs-1 mb-3">
|
||||
{book.description || testObject.description}
|
||||
</h2>
|
||||
|
||||
<div className="text-center mb-4">
|
||||
<img
|
||||
src={book.linkOnImage}
|
||||
alt="Обложка книги 1984"
|
||||
className="img-fluid rounded"
|
||||
style={{ maxWidth: "300px" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 p-3 --bg-medium rounded">
|
||||
<p className="mb-0">
|
||||
<strong>Автор:</strong> {book.author}
|
||||
<br />
|
||||
<strong>Жанр:</strong> {book.genre}
|
||||
<br />
|
||||
<strong>Год издания:</strong> {book.year || testObject.year}
|
||||
</p>
|
||||
</div>
|
||||
{book.tags && book.tags.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<h5 className="text-accent mb-3" style={{ color: "#00adb5" }}>
|
||||
Теги:
|
||||
</h5>
|
||||
<div className="d-flex flex-wrap gap-2">
|
||||
{book.tags.map((tag, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="badge bg-info text-dark px-3 py-2"
|
||||
style={{ backgroundColor: "#00adb5", color: "#222831" }}
|
||||
>
|
||||
{tag.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-4">
|
||||
<h5 className="text-accent mb-3" style={{ color: "#00adb5" }}>
|
||||
Аннотация:
|
||||
</h5>
|
||||
<p className="lh-base">{book.annotation}</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<a
|
||||
href="#"
|
||||
className="btn btn-primary px-5 py-2 fw-bold"
|
||||
style={{
|
||||
backgroundColor: "#00adb5",
|
||||
borderColor: "#00adb5",
|
||||
color: "#222831",
|
||||
}}
|
||||
>
|
||||
Читать книгу
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
);
|
||||
}
|
||||
|
||||
export default BookCardInfo;
|
||||
62
online-library/src/Choice/Choice.jsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { bookAPI } from "../ApiRequest/ApiClient";
|
||||
import axios from "axios";
|
||||
import { Alert } from "react-bootstrap";
|
||||
import CustomList from "./CustomList/CustomList";
|
||||
import { Spinner } from "react-bootstrap";
|
||||
|
||||
function Choice() {
|
||||
const [genres, setGenres] = useState([]);
|
||||
const [books, setBooks] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [message, setMessage] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const responseGenres = await bookAPI.getBooksGenre();
|
||||
const responseBooks = await bookAPI.getAll({ size: 100 });
|
||||
if (responseGenres.status === 200) {
|
||||
setGenres(responseGenres.data);
|
||||
}
|
||||
if (responseBooks.status === 200) {
|
||||
setBooks(responseBooks.data.content);
|
||||
}
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
setError(error.response.data.message);
|
||||
} else {
|
||||
setError("Ошибка загрузки с сервера");
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
}, []);
|
||||
books.map((book) => console.log(book.genre));
|
||||
if (loading) return <Spinner animation="border" size="sm" />;
|
||||
return (
|
||||
<main className="container">
|
||||
<h1 className="text-center mb-5" style={{ color: "var(--accent)" }}>
|
||||
Книги по тематикам
|
||||
</h1>
|
||||
{console.log(books + " ЭТО КНИГИ")}
|
||||
{error && <Alert variant="danger">{error}</Alert>}
|
||||
{message && <Alert variant="success">{message}</Alert>}
|
||||
<div className="row g-4">
|
||||
{genres.map((genre, index) => (
|
||||
<CustomList
|
||||
key={index}
|
||||
genre={genre}
|
||||
listOfElements={books.filter((book) => book.genre === genre)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default Choice;
|
||||
5
online-library/src/Choice/CustomList/CustomList.css
Normal file
@@ -0,0 +1,5 @@
|
||||
.category-card {
|
||||
background-color: var(--bg-medium);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
20
online-library/src/Choice/CustomList/CustomList.jsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import ElementList from "../ElementList/ElementList";
|
||||
import "./CustomList.css";
|
||||
|
||||
function CustomList({ genre, listOfElements }) {
|
||||
return (
|
||||
<div id={genre} className="col-12">
|
||||
<div className="category-card p-4">
|
||||
<h2 className="mb-4" style={{ color: "var(--accent)" }}>
|
||||
{genre}
|
||||
</h2>
|
||||
{listOfElements.map((item, index) => (
|
||||
<ElementList key={index} book={item} />
|
||||
))}
|
||||
<div className="list-group"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CustomList;
|
||||
10
online-library/src/Choice/ElementList/ElementList.css
Normal file
@@ -0,0 +1,10 @@
|
||||
.book-item {
|
||||
background-color: var(--bg-dark);
|
||||
border-radius: 5px;
|
||||
transition: transform 0.2s;
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
.book-item:hover {
|
||||
transform: translateX(10px);
|
||||
}
|
||||
23
online-library/src/Choice/ElementList/ElementList.jsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { NavLink } from "react-router-dom";
|
||||
import "./ElementList.css"
|
||||
|
||||
function ElementList({ book }) {
|
||||
return (
|
||||
<div className="book-item list-group-item d-flex justify-content-between align-items-center mb-2">
|
||||
<span>{book.title}</span>
|
||||
|
||||
<NavLink
|
||||
to={`/infobook/${book.id}`}
|
||||
className="btn btn-sm"
|
||||
style={{
|
||||
backgroundColor: "var(--accent)",
|
||||
color: "var(--bg-dark)",
|
||||
}}
|
||||
>
|
||||
Читать
|
||||
</NavLink>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ElementList;
|
||||
3
online-library/src/Footer/Footer.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.footer-custom {
|
||||
background-color: var(--bg-medium);
|
||||
}
|
||||
43
online-library/src/Footer/Footer.jsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import "bootstrap/dist/css/bootstrap.css";
|
||||
import "./Footer.css"
|
||||
|
||||
|
||||
function Footer() {
|
||||
return (
|
||||
<footer className="footer-custom mt-5 p-4">
|
||||
<div className="container">
|
||||
<div className="row text-center">
|
||||
<div className="col-md-6 mb-4">
|
||||
<h3>Контакты</h3>
|
||||
<p>
|
||||
Москва, ул. Книжная, 12
|
||||
<br />
|
||||
Часы работы: 10:00 - 20:00
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<h3>Соцсети</h3>
|
||||
<div className="d-flex justify-content-center gap-3">
|
||||
<a href="https://vk.ru">
|
||||
<img
|
||||
src={`${process.env.PUBLIC_URL}/resources/vk.svg`}
|
||||
alt="VK"
|
||||
style={{ width: 32 }}
|
||||
/>
|
||||
</a>
|
||||
<a href="https://lichess.org">
|
||||
<img
|
||||
src={`${process.env.PUBLIC_URL}/resources/telegram.svg`}
|
||||
alt="Telegram"
|
||||
style={{ width: 32 }}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
export default Footer;
|
||||
54
online-library/src/Lk/Lk.jsx
Normal file
@@ -0,0 +1,54 @@
|
||||
function Lk() {
|
||||
return (
|
||||
<main className="container">
|
||||
<h1 className="text-center mb-5" style={{ color: "var(--accent)" }}>
|
||||
Личный кабинет
|
||||
</h1>
|
||||
|
||||
<div className="row justify-content-center">
|
||||
<div className="col-md-8 col-lg-6">
|
||||
<form className="form-custom">
|
||||
<div className="mb-4">
|
||||
<label className="form-label">Имя</label>
|
||||
<input type="text" className="form-control" value="Иван" />
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="form-label">Фамилия</label>
|
||||
<input type="text" className="form-control" value="Иванов" />
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="form-label">Почта</label>
|
||||
<input
|
||||
type="email"
|
||||
className="form-control"
|
||||
value="ivan@example.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="form-label">Город</label>
|
||||
<input type="text" className="form-control" value="Москва" />
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="form-label">Телефон</label>
|
||||
<input
|
||||
type="tel"
|
||||
className="form-control"
|
||||
value="+7 (999) 123-45-67"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button className="btn btn-custom w-100 py-2">
|
||||
Сохранить изменения
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default Lk;
|
||||
337
online-library/src/Main/EnhancedForumPage.jsx
Normal file
@@ -0,0 +1,337 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, Badge, ProgressBar, ListGroup, Button } from 'react-bootstrap';
|
||||
|
||||
// Компонент популярных тем с расширенной функциональностью
|
||||
const PopularTopics = ({ posts }) => {
|
||||
const [trendingHashtags, setTrendingHashtags] = useState([]);
|
||||
const [hotTopics, setHotTopics] = useState([]);
|
||||
const [weeklyLeaderboard, setWeeklyLeaderboard] = useState([]);
|
||||
|
||||
// Анализируем данные для вычисления трендов
|
||||
useEffect(() => {
|
||||
// Анализ популярных хештегов
|
||||
const hashtagCount = {};
|
||||
posts.forEach(post => {
|
||||
post.hashtags.forEach(tag => {
|
||||
hashtagCount[tag] = (hashtagCount[tag] || 0) + 1 + post.likes * 0.1 + post.comments.length * 0.2;
|
||||
});
|
||||
});
|
||||
|
||||
const trending = Object.entries(hashtagCount)
|
||||
.sort(([,a], [,b]) => b - a)
|
||||
.slice(0, 8)
|
||||
.map(([tag, score]) => ({ tag, score }));
|
||||
|
||||
setTrendingHashtags(trending);
|
||||
|
||||
// Горячие темы (посты с наибольшей активностью)
|
||||
const hot = posts
|
||||
.map(post => ({
|
||||
...post,
|
||||
activityScore: post.likes + post.comments.length * 2
|
||||
}))
|
||||
.sort((a, b) => b.activityScore - a.activityScore)
|
||||
.slice(0, 5);
|
||||
|
||||
setHotTopics(hot);
|
||||
|
||||
// Топ пользователей (заглушка - в реальном приложении бралось бы из API)
|
||||
setWeeklyLeaderboard([
|
||||
{ username: "alex_dev", posts: 12, likes: 145 },
|
||||
{ username: "web_designer", posts: 8, likes: 98 },
|
||||
{ username: "react_fan", posts: 6, likes: 87 },
|
||||
{ username: "code_master", posts: 5, likes: 76 },
|
||||
{ username: "ui_guru", posts: 4, likes: 65 }
|
||||
]);
|
||||
}, [posts]);
|
||||
|
||||
const getTrendingLevel = (score, maxScore) => {
|
||||
const percentage = (score / maxScore) * 100;
|
||||
if (percentage > 80) return "danger";
|
||||
if (percentage > 60) return "warning";
|
||||
if (percentage > 40) return "info";
|
||||
return "secondary";
|
||||
};
|
||||
|
||||
const maxScore = trendingHashtags[0]?.score || 1;
|
||||
|
||||
return (
|
||||
<div className="popular-topics-section">
|
||||
{/* Трендовые хештеги */}
|
||||
<Card className="mb-4" style={{ backgroundColor: 'var(--bg-medium)', color: 'var(--text-light)' }}>
|
||||
<Card.Header style={{ borderBottom: '2px solid var(--accent)' }}>
|
||||
<h5 className="mb-0">🚀 Трендовые хештеги</h5>
|
||||
</Card.Header>
|
||||
<Card.Body>
|
||||
{trendingHashtags.map(({ tag, score }, index) => (
|
||||
<div key={tag} className="mb-3">
|
||||
<div className="d-flex justify-content-between align-items-center mb-1">
|
||||
<Badge
|
||||
bg={getTrendingLevel(score, maxScore)}
|
||||
style={{ cursor: 'pointer', fontSize: '0.85rem' }}
|
||||
>
|
||||
{tag}
|
||||
</Badge>
|
||||
<small className="text-muted">{Math.round(score)} очков</small>
|
||||
</div>
|
||||
<ProgressBar
|
||||
now={(score / maxScore) * 100}
|
||||
variant={getTrendingLevel(score, maxScore)}
|
||||
style={{ height: '4px' }}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</Card.Body>
|
||||
</Card>
|
||||
|
||||
{/* Горячие обсуждения */}
|
||||
<Card className="mb-4" style={{ backgroundColor: 'var(--bg-medium)', color: 'var(--text-light)' }}>
|
||||
<Card.Header style={{ borderBottom: '2px solid var(--accent)' }}>
|
||||
<h5 className="mb-0">🔥 Горячие обсуждения</h5>
|
||||
</Card.Header>
|
||||
<Card.Body>
|
||||
<ListGroup variant="flush">
|
||||
{hotTopics.map((topic, index) => (
|
||||
<ListGroup.Item
|
||||
key={topic.id}
|
||||
style={{
|
||||
backgroundColor: 'transparent',
|
||||
borderColor: 'var(--bg-dark)',
|
||||
color: 'var(--text-light)'
|
||||
}}
|
||||
className="px-0"
|
||||
>
|
||||
<div className="d-flex align-items-start">
|
||||
<Badge
|
||||
bg="accent"
|
||||
className="me-2 mt-1"
|
||||
style={{ backgroundColor: 'var(--accent)' }}
|
||||
>
|
||||
{index + 1}
|
||||
</Badge>
|
||||
<div className="flex-grow-1">
|
||||
<div
|
||||
className="fw-bold topic-title"
|
||||
style={{
|
||||
fontSize: '0.9rem',
|
||||
cursor: 'pointer',
|
||||
color: 'var(--text-light)'
|
||||
}}
|
||||
>
|
||||
{topic.title}
|
||||
</div>
|
||||
<div className="d-flex justify-content-between mt-1">
|
||||
<small className="text-muted">@{topic.author}</small>
|
||||
<small className="text-muted">
|
||||
{topic.likes} ❤️ • {topic.comments.length} 💬
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ListGroup.Item>
|
||||
))}
|
||||
</ListGroup>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
|
||||
{/* Таблица лидеров */}
|
||||
<Card className="mb-4" style={{ backgroundColor: 'var(--bg-medium)', color: 'var(--text-light)' }}>
|
||||
<Card.Header style={{ borderBottom: '2px solid var(--accent)' }}>
|
||||
<h5 className="mb-0">🏆 Топ авторов недели</h5>
|
||||
</Card.Header>
|
||||
<Card.Body>
|
||||
<ListGroup variant="flush">
|
||||
{weeklyLeaderboard.map((user, index) => (
|
||||
<ListGroup.Item
|
||||
key={user.username}
|
||||
style={{
|
||||
backgroundColor: 'transparent',
|
||||
borderColor: 'var(--bg-dark)',
|
||||
color: 'var(--text-light)'
|
||||
}}
|
||||
className="px-0"
|
||||
>
|
||||
<div className="d-flex align-items-center justify-content-between">
|
||||
<div className="d-flex align-items-center">
|
||||
<div
|
||||
className="rank-badge me-2 d-flex align-items-center justify-content-center"
|
||||
style={{
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: index === 0 ? '#ffd700' :
|
||||
index === 1 ? '#c0c0c0' :
|
||||
index === 2 ? '#cd7f32' : 'var(--accent)',
|
||||
color: index < 3 ? 'var(--bg-dark)' : 'var(--text-light)',
|
||||
fontSize: '0.7rem',
|
||||
fontWeight: 'bold'
|
||||
}}
|
||||
>
|
||||
{index + 1}
|
||||
</div>
|
||||
<span>@{user.username}</span>
|
||||
</div>
|
||||
<div className="text-end">
|
||||
<div className="small text-muted">
|
||||
{user.posts} постов
|
||||
</div>
|
||||
<div className="small" style={{ color: 'var(--accent)' }}>
|
||||
{user.likes} лайков
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ListGroup.Item>
|
||||
))}
|
||||
</ListGroup>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
|
||||
{/* Рекомендуемые темы */}
|
||||
<Card style={{ backgroundColor: 'var(--bg-medium)', color: 'var(--text-light)' }}>
|
||||
<Card.Header style={{ borderBottom: '2px solid var(--accent)' }}>
|
||||
<h5 className="mb-0">💡 Рекомендуемые темы</h5>
|
||||
</Card.Header>
|
||||
<Card.Body>
|
||||
<div className="d-flex flex-wrap gap-2 mb-3">
|
||||
<Badge bg="secondary" style={{ cursor: 'pointer' }}>#react</Badge>
|
||||
<Badge bg="secondary" style={{ cursor: 'pointer' }}>#javascript</Badge>
|
||||
<Badge bg="secondary" style={{ cursor: 'pointer' }}>#webdev</Badge>
|
||||
<Badge bg="secondary" style={{ cursor: 'pointer' }}>#css</Badge>
|
||||
<Badge bg="secondary" style={{ cursor: 'pointer' }}>#beginners</Badge>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline-accent"
|
||||
size="sm"
|
||||
className="w-100"
|
||||
style={{
|
||||
borderColor: 'var(--accent)',
|
||||
color: 'var(--accent)'
|
||||
}}
|
||||
>
|
||||
Показать все темы
|
||||
</Button>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Компонент статистики сообщества
|
||||
const CommunityStats = ({ posts }) => {
|
||||
const totalPosts = posts.length;
|
||||
const totalComments = posts.reduce((sum, post) => sum + post.comments.length, 0);
|
||||
const totalLikes = posts.reduce((sum, post) => sum + post.likes, 0);
|
||||
|
||||
// Собираем уникальных авторов
|
||||
const uniqueAuthors = [...new Set(posts.map(post => post.author))];
|
||||
|
||||
return (
|
||||
<Card className="mb-4" style={{ backgroundColor: 'var(--bg-medium)', color: 'var(--text-light)' }}>
|
||||
<Card.Header style={{ borderBottom: '2px solid var(--accent)' }}>
|
||||
<h5 className="mb-0">📊 Статистика сообщества</h5>
|
||||
</Card.Header>
|
||||
<Card.Body>
|
||||
<div className="row text-center">
|
||||
<div className="col-4">
|
||||
<div style={{ color: 'var(--accent)', fontSize: '1.5rem', fontWeight: 'bold' }}>
|
||||
{totalPosts}
|
||||
</div>
|
||||
<small className="text-muted">Постов</small>
|
||||
</div>
|
||||
<div className="col-4">
|
||||
<div style={{ color: 'var(--accent)', fontSize: '1.5rem', fontWeight: 'bold' }}>
|
||||
{totalComments}
|
||||
</div>
|
||||
<small className="text-muted">Комментариев</small>
|
||||
</div>
|
||||
<div className="col-4">
|
||||
<div style={{ color: 'var(--accent)', fontSize: '1.5rem', fontWeight: 'bold' }}>
|
||||
{totalLikes}
|
||||
</div>
|
||||
<small className="text-muted">Лайков</small>
|
||||
</div>
|
||||
</div>
|
||||
<hr style={{ borderColor: 'var(--bg-dark)' }} />
|
||||
<div className="text-center">
|
||||
<small className="text-muted">
|
||||
{uniqueAuthors.length} активных участников
|
||||
</small>
|
||||
</div>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
// Компонент событий и активностей
|
||||
const RecentActivity = () => {
|
||||
const activities = [
|
||||
{ type: 'new_post', user: 'react_fan', topic: 'React Hooks', time: '5 мин назад' },
|
||||
{ type: 'comment', user: 'web_designer', topic: 'Цветовые схемы', time: '12 мин назад' },
|
||||
{ type: 'like', user: 'code_master', topic: 'Оптимизация', time: '25 мин назад' },
|
||||
{ type: 'follow', user: 'ui_guru', topic: 'Новый участник', time: '1 час назад' }
|
||||
];
|
||||
|
||||
const getActivityIcon = (type) => {
|
||||
switch (type) {
|
||||
case 'new_post': return '📝';
|
||||
case 'comment': return '💬';
|
||||
case 'like': return '❤️';
|
||||
case 'follow': return '👤';
|
||||
default: return '🔔';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card style={{ backgroundColor: 'var(--bg-medium)', color: 'var(--text-light)' }}>
|
||||
<Card.Header style={{ borderBottom: '2px solid var(--accent)' }}>
|
||||
<h5 className="mb-0">🔔 Последняя активность</h5>
|
||||
</Card.Header>
|
||||
<Card.Body>
|
||||
<ListGroup variant="flush">
|
||||
{activities.map((activity, index) => (
|
||||
<ListGroup.Item
|
||||
key={index}
|
||||
style={{
|
||||
backgroundColor: 'transparent',
|
||||
borderColor: 'var(--bg-dark)',
|
||||
color: 'var(--text-light)'
|
||||
}}
|
||||
className="px-0"
|
||||
>
|
||||
<div className="d-flex align-items-start">
|
||||
<span className="me-2" style={{ fontSize: '1.1rem' }}>
|
||||
{getActivityIcon(activity.type)}
|
||||
</span>
|
||||
<div className="flex-grow-1">
|
||||
<div className="small">
|
||||
<strong>@{activity.user}</strong>
|
||||
{activity.type === 'new_post' && ' создал пост'}
|
||||
{activity.type === 'comment' && ' прокомментировал'}
|
||||
{activity.type === 'like' && ' лайкнул'}
|
||||
{activity.type === 'follow' && ' присоединился'}
|
||||
</div>
|
||||
<div className="small text-muted">
|
||||
"{activity.topic}" • {activity.time}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ListGroup.Item>
|
||||
))}
|
||||
</ListGroup>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
// Обновленный компонент ForumPage с новыми блоками
|
||||
const EnhancedForumPage = ({ posts }) => {
|
||||
return (
|
||||
<div className="enhanced-forum-sidebar">
|
||||
<CommunityStats posts={posts} />
|
||||
<PopularTopics posts={posts} />
|
||||
<RecentActivity />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EnhancedForumPage;
|
||||
249
online-library/src/Main/ForumPage.jsx
Normal file
@@ -0,0 +1,249 @@
|
||||
import React, { useState } from "react";
|
||||
import { Container, Row, Col, Nav, Form, Button } from "react-bootstrap";
|
||||
import "bootstrap/dist/css/bootstrap.min.css";
|
||||
import Post from "./Post";
|
||||
import NewPostModal from "./NewPostModal";
|
||||
import EnhancedForumPage from "./EnhancedForumPage";
|
||||
|
||||
// Стили с использованием вашей цветовой схемы
|
||||
const customStyles = `
|
||||
:root {
|
||||
--bg-dark: #222831;
|
||||
--bg-medium: #393e46;
|
||||
--accent: #00adb5;
|
||||
--text-light: #eeeeee;
|
||||
}
|
||||
|
||||
.forum-container {
|
||||
background-color: var(--bg-dark);
|
||||
color: var(--text-light);
|
||||
min-height: 100vh;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.filter-menu, .popular-topics, .user-stats, .recent-activity {
|
||||
background-color: var(--bg-medium);
|
||||
border-radius: 10px;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.post-card {
|
||||
background-color: var(--bg-medium);
|
||||
border: none;
|
||||
color: var(--text-light);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.post-card:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-accent {
|
||||
background-color: var(--accent);
|
||||
border: none;
|
||||
color: var(--bg-dark);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.btn-outline-accent {
|
||||
border: 1px solid var(--accent);
|
||||
color: var(--accent);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.btn-outline-accent:hover {
|
||||
background-color: var(--accent);
|
||||
color: var(--bg-dark);
|
||||
}
|
||||
|
||||
.like-btn:hover, .comment-toggle-btn:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.hashtag-filter {
|
||||
color: var(--accent);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.comment-item {
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.comment-item:hover {
|
||||
background-color: rgba(0, 173, 181, 0.1) !important;
|
||||
}
|
||||
`;
|
||||
|
||||
const ForumPage = () => {
|
||||
const [posts, setPosts] = useState([
|
||||
{
|
||||
id: 1,
|
||||
author: "alex_dev",
|
||||
title: "Мой первый пост на React Bootstrap",
|
||||
text: "Сегодня я начал изучать React Bootstrap и хочу поделиться своими впечатлениями. Очень удобная библиотека для быстрой разработки UI!",
|
||||
images: [
|
||||
"https://via.placeholder.com/600x300/00adb5/222831?text=React+Bootstrap",
|
||||
],
|
||||
likes: 24,
|
||||
comments: [
|
||||
"Отличный пост! Согласен с тобой полностью.",
|
||||
"А есть какие-то конкретные примеры использования?",
|
||||
],
|
||||
hashtags: ["#react", "#bootstrap", "#webdev"],
|
||||
commentsDisabled: false,
|
||||
createdAt: "2024-01-15T10:30:00Z",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
author: "web_designer",
|
||||
title: "Советы по цветовым схемам",
|
||||
text: "Хочу поделиться несколькими советами по выбору цветовых схем для веб-приложений. Контраст и читаемость - наше всё!",
|
||||
images: null,
|
||||
likes: 15,
|
||||
comments: [],
|
||||
hashtags: ["#design", "#ui", "#colors"],
|
||||
commentsDisabled: false,
|
||||
createdAt: "2024-01-14T16:45:00Z",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
author: "web_designer",
|
||||
title: "Советы по цветовым схемам",
|
||||
text: "Хочу поделиться несколькими советами по выбору цветовых схем для веб-приложений. Контраст и читаемость - наше всё!",
|
||||
images: null,
|
||||
likes: 15,
|
||||
comments: [],
|
||||
hashtags: ["#design", "#ui", "#colors"],
|
||||
commentsDisabled: true,
|
||||
createdAt: "2024-01-14T16:45:00Z",
|
||||
},
|
||||
]);
|
||||
|
||||
const [showNewPostModal, setShowNewPostModal] = useState(false);
|
||||
const [selectedHashtags, setSelectedHashtags] = useState([]);
|
||||
|
||||
const allHashtags = [...new Set(posts.flatMap((post) => post.hashtags))];
|
||||
|
||||
const filteredPosts =
|
||||
selectedHashtags.length > 0
|
||||
? posts.filter((post) =>
|
||||
post.hashtags.some((tag) => selectedHashtags.includes(tag))
|
||||
)
|
||||
: posts;
|
||||
|
||||
const handleLike = (postId) => {
|
||||
setPosts(
|
||||
posts.map((post) =>
|
||||
post.id === postId ? { ...post, likes: post.likes + 1 } : post
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const handleAddComment = (postId, commentText) => {
|
||||
setPosts(
|
||||
posts.map((post) =>
|
||||
post.id === postId
|
||||
? { ...post, comments: [...post.comments, commentText] }
|
||||
: post
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const handleAddNewPost = (newPost) => {
|
||||
setPosts([newPost, ...posts]);
|
||||
};
|
||||
|
||||
const toggleHashtag = (hashtag) => {
|
||||
setSelectedHashtags((prev) =>
|
||||
prev.includes(hashtag)
|
||||
? prev.filter((t) => t !== hashtag)
|
||||
: [...prev, hashtag]
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>{customStyles}</style>
|
||||
<div className="forum-container">
|
||||
<Container>
|
||||
<Row>
|
||||
{/* Левая колонка - меню фильтрации */}
|
||||
<Col md={3}>
|
||||
<div className="filter-menu">
|
||||
<h5>Фильтр по хештегам</h5>
|
||||
{allHashtags.map((tag) => (
|
||||
<Form.Check
|
||||
key={tag}
|
||||
type="checkbox"
|
||||
label={tag}
|
||||
checked={selectedHashtags.includes(tag)}
|
||||
onChange={() => toggleHashtag(tag)}
|
||||
className="hashtag-filter"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="popular-topics">
|
||||
<h5>Популярные темы</h5>
|
||||
<Nav className="flex-column">
|
||||
<Nav.Link>React Components</Nav.Link>
|
||||
<Nav.Link>Bootstrap Layouts</Nav.Link>
|
||||
<Nav.Link>CSS Tricks</Nav.Link>
|
||||
</Nav>
|
||||
</div>
|
||||
|
||||
<div className="user-stats">
|
||||
<h5>Статистика</h5>
|
||||
<p>Постов: {posts.length}</p>
|
||||
<p>
|
||||
Комментариев:{" "}
|
||||
{posts.reduce((acc, post) => acc + post.comments.length, 0)}
|
||||
</p>
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
{/* Центральная колонка - посты */}
|
||||
<Col md={6}>
|
||||
{filteredPosts.map((post) => (
|
||||
<Post
|
||||
key={post.id}
|
||||
post={post}
|
||||
onLike={handleLike}
|
||||
onAddComment={handleAddComment}
|
||||
/>
|
||||
))}
|
||||
</Col>
|
||||
|
||||
{/* Правая колонка - действия пользователя */}
|
||||
<Col md={3}>
|
||||
<div className="d-grid gap-2 mb-4">
|
||||
<Button
|
||||
variant="accent"
|
||||
size="lg"
|
||||
onClick={() => setShowNewPostModal(true)}
|
||||
>
|
||||
+ Новый пост
|
||||
</Button>
|
||||
<Button variant="outline-accent" size="lg">
|
||||
👤 Мой профиль
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Новые расширенные блоки */}
|
||||
<EnhancedForumPage posts={posts} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</div>
|
||||
|
||||
<NewPostModal
|
||||
show={showNewPostModal}
|
||||
onHide={() => setShowNewPostModal(false)}
|
||||
onAddPost={handleAddNewPost}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ForumPage;
|
||||
41
online-library/src/Main/Main.jsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import BookCard from "../BookCard/BookCard";
|
||||
import { useEffect, useState } from "react";
|
||||
import { bookAPI } from "../ApiRequest/ApiClient";
|
||||
import { Spinner } from "react-bootstrap";
|
||||
|
||||
function Main() {
|
||||
const [bookData, setBookData] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("Вызвалася useEffect в Main.jsx UYGREFGJVHIEGHDJLFSHDGJSGFJL");
|
||||
}, [error]);
|
||||
useEffect(() => {
|
||||
const fetchBooks = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await bookAPI.getAll();
|
||||
setBookData(response.data.content);
|
||||
} catch (error) {
|
||||
setError(error.message);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
fetchBooks();
|
||||
}, []);
|
||||
if (isLoading) return <Spinner animation="border" size="sm" />;
|
||||
if (error) return <div> Ошибка: {error}</div>;
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
|
||||
{bookData.map((book) => (
|
||||
<BookCard key={book.id} book={book}></BookCard>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Main;
|
||||
292
online-library/src/Main/NewPostModal.jsx
Normal file
@@ -0,0 +1,292 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { Modal, Form, Button, Row, Col, Badge, Image } from 'react-bootstrap';
|
||||
|
||||
const NewPostModal = ({ show, onHide, onAddPost }) => {
|
||||
const [newPost, setNewPost] = useState({
|
||||
title: '',
|
||||
text: '',
|
||||
hashtags: [],
|
||||
images: [],
|
||||
commentsDisabled: false
|
||||
});
|
||||
|
||||
const [currentHashtag, setCurrentHashtag] = useState('');
|
||||
const fileInputRef = useRef(null);
|
||||
|
||||
const handleAddHashtag = () => {
|
||||
if (currentHashtag.trim() && !newPost.hashtags.includes(currentHashtag)) {
|
||||
setNewPost({
|
||||
...newPost,
|
||||
hashtags: [...newPost.hashtags, currentHashtag.trim()]
|
||||
});
|
||||
setCurrentHashtag('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveHashtag = (hashtagToRemove) => {
|
||||
setNewPost({
|
||||
...newPost,
|
||||
hashtags: newPost.hashtags.filter(hashtag => hashtag !== hashtagToRemove)
|
||||
});
|
||||
};
|
||||
|
||||
const handleImageUpload = (e) => {
|
||||
const files = Array.from(e.target.files);
|
||||
const imageUrls = files.map(file => URL.createObjectURL(file));
|
||||
|
||||
setNewPost({
|
||||
...newPost,
|
||||
images: [...newPost.images, ...imageUrls]
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemoveImage = (imageToRemove) => {
|
||||
setNewPost({
|
||||
...newPost,
|
||||
images: newPost.images.filter(image => image !== imageToRemove)
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (newPost.title.trim() && newPost.text.trim()) {
|
||||
onAddPost({
|
||||
...newPost,
|
||||
id: Date.now(),
|
||||
author: "Текущий пользователь",
|
||||
likes: 0,
|
||||
comments: [],
|
||||
createdAt: new Date().toISOString()
|
||||
});
|
||||
|
||||
// Сброс формы
|
||||
setNewPost({
|
||||
title: '',
|
||||
text: '',
|
||||
hashtags: [],
|
||||
images: [],
|
||||
commentsDisabled: false
|
||||
});
|
||||
setCurrentHashtag('');
|
||||
onHide();
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyPress = (e) => {
|
||||
if (e.key === 'Enter' && currentHashtag.trim()) {
|
||||
e.preventDefault();
|
||||
handleAddHashtag();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
show={show}
|
||||
onHide={onHide}
|
||||
size="lg"
|
||||
centered
|
||||
style={{ fontFamily: 'Arial, sans-serif' }}
|
||||
>
|
||||
<Modal.Header
|
||||
closeButton
|
||||
style={{
|
||||
backgroundColor: 'var(--bg-medium)',
|
||||
color: 'var(--text-light)',
|
||||
borderBottom: '1px solid var(--accent)'
|
||||
}}
|
||||
>
|
||||
<Modal.Title>Создать новый пост</Modal.Title>
|
||||
</Modal.Header>
|
||||
|
||||
<Modal.Body style={{ backgroundColor: 'var(--bg-medium)', color: 'var(--text-light)' }}>
|
||||
<Form>
|
||||
{/* Заголовок поста */}
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label style={{ color: 'var(--text-light)' }}>Заголовок поста *</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
placeholder="Введите заголовок..."
|
||||
value={newPost.title}
|
||||
onChange={(e) => setNewPost({...newPost, title: e.target.value})}
|
||||
style={{
|
||||
backgroundColor: 'var(--bg-dark)',
|
||||
border: '1px solid var(--accent)',
|
||||
color: 'var(--text-light)'
|
||||
}}
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
{/* Хештеги */}
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label style={{ color: 'var(--text-light)' }}>Хештеги</Form.Label>
|
||||
<div className="d-flex gap-2 mb-2">
|
||||
<Form.Control
|
||||
type="text"
|
||||
placeholder="Добавьте хештег..."
|
||||
value={currentHashtag}
|
||||
onChange={(e) => setCurrentHashtag(e.target.value)}
|
||||
onKeyPress={handleKeyPress}
|
||||
style={{
|
||||
backgroundColor: 'var(--bg-dark)',
|
||||
border: '1px solid var(--accent)',
|
||||
color: 'var(--text-light)'
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="outline-accent"
|
||||
onClick={handleAddHashtag}
|
||||
disabled={!currentHashtag.trim()}
|
||||
>
|
||||
Добавить
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Список добавленных хештегов */}
|
||||
<div className="d-flex flex-wrap gap-2">
|
||||
{newPost.hashtags.map((hashtag, index) => (
|
||||
<Badge
|
||||
key={index}
|
||||
bg="accent"
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
backgroundColor: 'var(--accent)',
|
||||
fontSize: '0.9rem',
|
||||
padding: '0.5rem 0.75rem'
|
||||
}}
|
||||
onClick={() => handleRemoveHashtag(hashtag)}
|
||||
>
|
||||
{hashtag} ×
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</Form.Group>
|
||||
|
||||
{/* Текст поста */}
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label style={{ color: 'var(--text-light)' }}>Текст поста *</Form.Label>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
rows={4}
|
||||
placeholder="Напишите ваш пост..."
|
||||
value={newPost.text}
|
||||
onChange={(e) => setNewPost({...newPost, text: e.target.value})}
|
||||
style={{
|
||||
backgroundColor: 'var(--bg-dark)',
|
||||
border: '1px solid var(--accent)',
|
||||
color: 'var(--text-light)',
|
||||
resize: 'vertical'
|
||||
}}
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
{/* Загрузка изображений */}
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label style={{ color: 'var(--text-light)' }}>Изображения</Form.Label>
|
||||
<Form.Control
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
accept="image/*"
|
||||
onChange={handleImageUpload}
|
||||
style={{
|
||||
backgroundColor: 'var(--bg-dark)',
|
||||
border: '1px solid var(--accent)',
|
||||
color: 'var(--text-light)'
|
||||
}}
|
||||
/>
|
||||
<Form.Text style={{ color: 'var(--text-light)' }}>
|
||||
Вы можете загрузить несколько изображений
|
||||
</Form.Text>
|
||||
|
||||
{/* Преview изображений */}
|
||||
{newPost.images.length > 0 && (
|
||||
<Row className="mt-3">
|
||||
{newPost.images.map((image, index) => (
|
||||
<Col xs={6} md={4} key={index} className="mb-2 position-relative">
|
||||
<Image
|
||||
src={image}
|
||||
alt={`Preview ${index + 1}`}
|
||||
fluid
|
||||
rounded
|
||||
style={{
|
||||
border: '2px solid var(--accent)',
|
||||
height: '100px',
|
||||
objectFit: 'cover',
|
||||
width: '100%'
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="danger"
|
||||
size="sm"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '5px',
|
||||
right: '5px',
|
||||
borderRadius: '50%',
|
||||
width: '25px',
|
||||
height: '25px',
|
||||
padding: 0,
|
||||
fontSize: '12px'
|
||||
}}
|
||||
onClick={() => handleRemoveImage(image)}
|
||||
>
|
||||
×
|
||||
</Button>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
)}
|
||||
</Form.Group>
|
||||
|
||||
{/* Настройки комментариев */}
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Check
|
||||
type="checkbox"
|
||||
label="Отключить комментарии"
|
||||
checked={newPost.commentsDisabled}
|
||||
onChange={(e) => setNewPost({
|
||||
...newPost,
|
||||
commentsDisabled: e.target.checked
|
||||
})}
|
||||
style={{ color: 'var(--text-light)' }}
|
||||
/>
|
||||
<Form.Text style={{ color: 'var(--text-light)' }}>
|
||||
При отмеченной опции другие пользователи не смогут комментировать этот пост
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
</Modal.Body>
|
||||
|
||||
<Modal.Footer style={{
|
||||
backgroundColor: 'var(--bg-medium)',
|
||||
borderTop: '1px solid var(--accent)'
|
||||
}}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={onHide}
|
||||
style={{
|
||||
backgroundColor: 'var(--bg-dark)',
|
||||
border: '1px solid var(--text-light)',
|
||||
color: 'var(--text-light)'
|
||||
}}
|
||||
>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button
|
||||
variant="accent"
|
||||
onClick={handleSubmit}
|
||||
disabled={!newPost.title.trim() || !newPost.text.trim()}
|
||||
style={{
|
||||
backgroundColor: 'var(--accent)',
|
||||
border: 'none',
|
||||
color: 'var(--bg-dark)',
|
||||
fontWeight: 'bold'
|
||||
}}
|
||||
>
|
||||
Опубликовать
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewPostModal;
|
||||
189
online-library/src/Main/Post.jsx
Normal file
@@ -0,0 +1,189 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Card, Button, Form, InputGroup, Badge, Collapse } from 'react-bootstrap';
|
||||
|
||||
const Post = ({ post, onLike, onAddComment }) => {
|
||||
const [showComments, setShowComments] = useState(false);
|
||||
const [newComment, setNewComment] = useState('');
|
||||
|
||||
const handleSubmitComment = (e) => {
|
||||
e.preventDefault();
|
||||
if (newComment.trim()) {
|
||||
onAddComment(post.id, newComment);
|
||||
setNewComment('');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="post-card mb-4">
|
||||
<Card.Body>
|
||||
{/* Заголовок и автор */}
|
||||
<div className="d-flex justify-content-between align-items-start mb-3">
|
||||
<div>
|
||||
<Card.Title className="mb-1" style={{ color: 'var(--accent)' }}>
|
||||
{post.title}
|
||||
</Card.Title>
|
||||
<Card.Subtitle className="text-muted small">
|
||||
@{post.author} • {new Date(post.createdAt).toLocaleDateString()}
|
||||
</Card.Subtitle>
|
||||
</div>
|
||||
<div>
|
||||
{post.commentsDisabled && (
|
||||
<Badge bg="secondary" className="me-1">
|
||||
Комментарии отключены
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Хештеги */}
|
||||
{post.hashtags.length > 0 && (
|
||||
<div className="mb-3">
|
||||
{post.hashtags.map((tag, index) => (
|
||||
<Badge
|
||||
key={index}
|
||||
bg="secondary"
|
||||
className="me-1 mb-1"
|
||||
style={{
|
||||
backgroundColor: 'var(--accent)',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Текст поста */}
|
||||
<Card.Text className="mb-3" style={{ lineHeight: '1.5' }}>
|
||||
{post.text}
|
||||
</Card.Text>
|
||||
|
||||
{/* Изображения */}
|
||||
{post.images && post.images.length > 0 && (
|
||||
<div className="mb-3">
|
||||
{post.images.map((image, index) => (
|
||||
<img
|
||||
key={index}
|
||||
src={image}
|
||||
alt={`Post image ${index + 1}`}
|
||||
className="img-fluid rounded mb-2"
|
||||
style={{
|
||||
maxHeight: '400px',
|
||||
width: '100%',
|
||||
objectFit: 'cover'
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Статистика и действия */}
|
||||
<div className="d-flex justify-content-between align-items-center border-top border-bottom py-2 mb-3">
|
||||
<div className="d-flex align-items-center">
|
||||
<Button
|
||||
variant="link"
|
||||
className="p-0 me-3 like-btn"
|
||||
onClick={() => onLike(post.id)}
|
||||
style={{
|
||||
color: 'var(--text-light)',
|
||||
textDecoration: 'none'
|
||||
}}
|
||||
>
|
||||
<span style={{ color: 'var(--accent)', fontSize: '1.2rem' }}>❤️</span>
|
||||
<span className="ms-1">{post.likes}</span>
|
||||
</Button>
|
||||
|
||||
{!post.commentsDisabled && (
|
||||
<Button
|
||||
variant="link"
|
||||
className="p-0 comment-toggle-btn"
|
||||
onClick={() => setShowComments(!showComments)}
|
||||
style={{
|
||||
color: 'var(--text-light)',
|
||||
textDecoration: 'none'
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: '1.2rem' }}>💬</span>
|
||||
<span className="ms-1">{post.comments.length}</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!post.commentsDisabled && (
|
||||
<Button
|
||||
variant="outline-accent"
|
||||
size="sm"
|
||||
onClick={() => setShowComments(!showComments)}
|
||||
>
|
||||
{showComments ? 'Скрыть комментарии' : 'Показать комментарии'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Комментарии с анимацией раскрытия */}
|
||||
{!post.commentsDisabled && (
|
||||
<Collapse in={showComments}>
|
||||
<div>
|
||||
{/* Список комментариев */}
|
||||
{post.comments.length > 0 ? (
|
||||
<div className="comments-section mb-3">
|
||||
{post.comments.map((comment, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="comment-item p-2 mb-2 rounded"
|
||||
style={{
|
||||
backgroundColor: 'var(--bg-dark)',
|
||||
borderLeft: `3px solid var(--accent)`
|
||||
}}
|
||||
>
|
||||
<div className="d-flex justify-content-between align-items-start">
|
||||
<strong className="text-accent">User{index + 1}</strong>
|
||||
<small className="text-muted">
|
||||
{new Date().toLocaleTimeString()}
|
||||
</small>
|
||||
</div>
|
||||
<p className="mb-0 mt-1">{comment}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="text-center py-3 text-muted"
|
||||
style={{ backgroundColor: 'var(--bg-dark)', borderRadius: '8px' }}
|
||||
>
|
||||
Пока нет комментариев. Будьте первым!
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Форма добавления комментария */}
|
||||
<Form onSubmit={handleSubmitComment}>
|
||||
<InputGroup>
|
||||
<Form.Control
|
||||
placeholder="Напишите комментарий..."
|
||||
value={newComment}
|
||||
onChange={(e) => setNewComment(e.target.value)}
|
||||
style={{
|
||||
backgroundColor: 'var(--bg-dark)',
|
||||
border: '1px solid var(--accent)',
|
||||
color: 'var(--text-light)'
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="accent"
|
||||
type="submit"
|
||||
disabled={!newComment.trim()}
|
||||
>
|
||||
Отправить
|
||||
</Button>
|
||||
</InputGroup>
|
||||
</Form>
|
||||
</div>
|
||||
</Collapse>
|
||||
)}
|
||||
</Card.Body>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default Post;
|
||||
492
online-library/src/Main/ProfilePage.jsx
Normal file
@@ -0,0 +1,492 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Container, Row, Col, Card, Tab, Tabs, Badge, Button, Form, Modal, ProgressBar, ListGroup, Image } from 'react-bootstrap';
|
||||
|
||||
// Стили для страницы профиля
|
||||
const profileStyles = `
|
||||
.profile-container {
|
||||
background-color: var(--bg-dark);
|
||||
color: var(--text-light);
|
||||
min-height: 100vh;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.profile-header {
|
||||
background: linear-gradient(135deg, var(--bg-medium) 0%, var(--accent) 100%);
|
||||
border-radius: 15px;
|
||||
padding: 30px;
|
||||
margin-bottom: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.profile-avatar {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 50%;
|
||||
border: 4px solid var(--text-light);
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.profile-stats {
|
||||
background-color: var(--bg-medium);
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.profile-card {
|
||||
background-color: var(--bg-medium);
|
||||
border: none;
|
||||
color: var(--text-light);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.achievement-badge {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.5rem;
|
||||
margin: 0 auto 10px;
|
||||
}
|
||||
|
||||
.edit-profile-btn {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link {
|
||||
background-color: var(--bg-medium);
|
||||
color: var(--text-light);
|
||||
border: none;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link.active {
|
||||
background-color: var(--accent);
|
||||
color: var(--bg-dark);
|
||||
border: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.activity-item {
|
||||
border-left: 3px solid var(--accent);
|
||||
padding-left: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
`;
|
||||
|
||||
const ProfilePage = () => {
|
||||
const [user, setUser] = useState({
|
||||
id: 1,
|
||||
username: "alex_dev",
|
||||
name: "Алексей Петров",
|
||||
avatar: "https://via.placeholder.com/120/00adb5/222831?text=AP",
|
||||
coverImage: "https://via.placeholder.com/1200/300/393e46/00adb5?text=Profile+Cover",
|
||||
bio: "Frontend разработчик | React enthusiast | Люблю создавать красивые и функциональные интерфейсы",
|
||||
joinDate: "2023-05-15",
|
||||
location: "Москва, Россия",
|
||||
website: "https://alexdev-portfolio.ru",
|
||||
stats: {
|
||||
posts: 24,
|
||||
comments: 156,
|
||||
likes: 842,
|
||||
followers: 128,
|
||||
following: 64
|
||||
}
|
||||
});
|
||||
|
||||
const [userPosts, setUserPosts] = useState([
|
||||
{
|
||||
id: 1,
|
||||
title: "Мой первый пост на React Bootstrap",
|
||||
content: "Сегодня я начал изучать React Bootstrap и хочу поделиться своими впечатлениями...",
|
||||
likes: 24,
|
||||
comments: 8,
|
||||
hashtags: ["#react", "#bootstrap", "#webdev"],
|
||||
date: "2024-01-15",
|
||||
isPublished: true
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Советы по цветовым схемам",
|
||||
content: "Хочу поделиться несколькими советами по выбору цветовых схем для веб-приложений...",
|
||||
likes: 15,
|
||||
comments: 12,
|
||||
hashtags: ["#design", "#ui", "#colors"],
|
||||
date: "2024-01-14",
|
||||
isPublished: true
|
||||
}
|
||||
]);
|
||||
|
||||
const [achievements, setAchievements] = useState([
|
||||
{ id: 1, name: "Первый пост", icon: "📝", description: "Опубликовал первый пост", earned: true },
|
||||
{ id: 2, name: "Активный комментатор", icon: "💬", description: "Оставил 50 комментариев", earned: true },
|
||||
{ id: 3, name: "Популярный автор", icon: "🔥", description: "Получил 100 лайков", earned: true },
|
||||
{ id: 4, name: "Эксперт", icon: "🏆", description: "10 постов с более 20 лайками", earned: false },
|
||||
{ id: 5, name: "Социальная бабочка", icon: "🦋", description: "50 подписчиков", earned: true },
|
||||
{ id: 6, name: "Ветеран", icon: "🎖️", description: "На сайте более 6 месяцев", earned: false }
|
||||
]);
|
||||
|
||||
const [recentActivity, setRecentActivity] = useState([
|
||||
{ type: "post", content: "Опубликовал новый пост 'React Hooks Guide'", date: "2 часа назад" },
|
||||
{ type: "comment", content: "Прокомментировал пост 'CSS Grid vs Flexbox'", date: "5 часов назад" },
|
||||
{ type: "like", content: "Понравился пост 'JavaScript Best Practices'", date: "Вчера" },
|
||||
{ type: "achievement", content: "Получено достижение 'Популярный автор'", date: "2 дня назад" }
|
||||
]);
|
||||
|
||||
const [showEditModal, setShowEditModal] = useState(false);
|
||||
const [editForm, setEditForm] = useState({
|
||||
name: user.name,
|
||||
bio: user.bio,
|
||||
location: user.location,
|
||||
website: user.website
|
||||
});
|
||||
|
||||
const handleSaveProfile = () => {
|
||||
setUser({
|
||||
...user,
|
||||
...editForm
|
||||
});
|
||||
setShowEditModal(false);
|
||||
};
|
||||
|
||||
const getActivityIcon = (type) => {
|
||||
switch (type) {
|
||||
case 'post': return '📝';
|
||||
case 'comment': return '💬';
|
||||
case 'like': return '❤️';
|
||||
case 'achievement': return '🏆';
|
||||
default: return '🔔';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>{profileStyles}</style>
|
||||
<div className="profile-container">
|
||||
<Container>
|
||||
{/* Шапка профиля */}
|
||||
<Card className="profile-header">
|
||||
<div className="edit-profile-btn">
|
||||
<Button
|
||||
variant="outline-light"
|
||||
size="sm"
|
||||
onClick={() => setShowEditModal(true)}
|
||||
>
|
||||
✏️ Редактировать профиль
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Row className="align-items-end">
|
||||
<Col md="auto">
|
||||
<img
|
||||
src={user.avatar}
|
||||
alt="Avatar"
|
||||
className="profile-avatar"
|
||||
/>
|
||||
</Col>
|
||||
<Col>
|
||||
<h1 className="mb-1">{user.name}</h1>
|
||||
<p className="mb-1">@{user.username}</p>
|
||||
<p className="mb-2">{user.bio}</p>
|
||||
<div className="d-flex gap-3 text-sm">
|
||||
<span>📍 {user.location}</span>
|
||||
<span>🔗 {user.website}</span>
|
||||
<span>📅 На сайте с {new Date(user.joinDate).toLocaleDateString('ru-RU')}</span>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
<Row>
|
||||
{/* Левая колонка - статистика и достижения */}
|
||||
<Col md={4}>
|
||||
{/* Статистика */}
|
||||
<Card className="profile-stats mb-4">
|
||||
<h5 className="mb-3">📊 Статистика активности</h5>
|
||||
<Row className="text-center">
|
||||
<Col xs={4}>
|
||||
<div style={{ color: 'var(--accent)', fontSize: '1.5rem', fontWeight: 'bold' }}>
|
||||
{user.stats.posts}
|
||||
</div>
|
||||
<small className="text-muted">Постов</small>
|
||||
</Col>
|
||||
<Col xs={4}>
|
||||
<div style={{ color: 'var(--accent)', fontSize: '1.5rem', fontWeight: 'bold' }}>
|
||||
{user.stats.comments}
|
||||
</div>
|
||||
<small className="text-muted">Комментариев</small>
|
||||
</Col>
|
||||
<Col xs={4}>
|
||||
<div style={{ color: 'var(--accent)', fontSize: '1.5rem', fontWeight: 'bold' }}>
|
||||
{user.stats.likes}
|
||||
</div>
|
||||
<small className="text-muted">Лайков</small>
|
||||
</Col>
|
||||
</Row>
|
||||
<hr />
|
||||
<Row className="text-center">
|
||||
<Col xs={6}>
|
||||
<div style={{ color: 'var(--accent)', fontSize: '1.5rem', fontWeight: 'bold' }}>
|
||||
{user.stats.followers}
|
||||
</div>
|
||||
<small className="text-muted">Подписчиков</small>
|
||||
</Col>
|
||||
<Col xs={6}>
|
||||
<div style={{ color: 'var(--accent)', fontSize: '1.5rem', fontWeight: 'bold' }}>
|
||||
{user.stats.following}
|
||||
</div>
|
||||
<small className="text-muted">Подписок</small>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
{/* Достижения */}
|
||||
<Card className="profile-card">
|
||||
<Card.Header>
|
||||
<h5 className="mb-0">🏆 Достижения</h5>
|
||||
</Card.Header>
|
||||
<Card.Body>
|
||||
<Row>
|
||||
{achievements.map(achievement => (
|
||||
<Col xs={6} key={achievement.id} className="text-center mb-3">
|
||||
<div
|
||||
className="achievement-badge"
|
||||
style={{
|
||||
backgroundColor: achievement.earned ? 'var(--accent)' : 'var(--bg-dark)',
|
||||
opacity: achievement.earned ? 1 : 0.5
|
||||
}}
|
||||
>
|
||||
{achievement.icon}
|
||||
</div>
|
||||
<div className="small">
|
||||
<strong>{achievement.name}</strong>
|
||||
<div className="text-muted" style={{ fontSize: '0.7rem' }}>
|
||||
{achievement.description}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
<ProgressBar
|
||||
now={achievements.filter(a => a.earned).length / achievements.length * 100}
|
||||
variant="accent"
|
||||
className="mt-2"
|
||||
/>
|
||||
<div className="text-center small text-muted mt-1">
|
||||
{achievements.filter(a => a.earned).length} из {achievements.length} достижений
|
||||
</div>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* Правая колонка - вкладки с контентом */}
|
||||
<Col md={8}>
|
||||
<Card className="profile-card">
|
||||
<Card.Body className="p-0">
|
||||
<Tabs defaultActiveKey="posts" className="px-3 pt-3">
|
||||
{/* Вкладка с постами */}
|
||||
<Tab eventKey="posts" title="📝 Мои посты">
|
||||
<div className="p-3">
|
||||
{userPosts.map(post => (
|
||||
<Card key={post.id} className="mb-3" style={{ backgroundColor: 'var(--bg-dark)' }}>
|
||||
<Card.Body>
|
||||
<div className="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<h6>{post.title}</h6>
|
||||
<p className="mb-2 text-muted small">{post.content}</p>
|
||||
<div className="mb-2">
|
||||
{post.hashtags.map(tag => (
|
||||
<Badge key={tag} bg="secondary" className="me-1">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-end">
|
||||
<div className="small text-muted">
|
||||
{new Date(post.date).toLocaleDateString('ru-RU')}
|
||||
</div>
|
||||
<div className="d-flex gap-2 mt-1">
|
||||
<small className="text-muted">❤️ {post.likes}</small>
|
||||
<small className="text-muted">💬 {post.comments}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="d-flex gap-2 mt-2">
|
||||
<Button variant="outline-accent" size="sm">Редактировать</Button>
|
||||
<Button variant="outline-danger" size="sm">Удалить</Button>
|
||||
{!post.isPublished && (
|
||||
<Button variant="accent" size="sm">Опубликовать</Button>
|
||||
)}
|
||||
</div>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</Tab>
|
||||
|
||||
{/* Вкладка с активностью */}
|
||||
<Tab eventKey="activity" title="🔔 Активность">
|
||||
<div className="p-3">
|
||||
<ListGroup variant="flush">
|
||||
{recentActivity.map((activity, index) => (
|
||||
<ListGroup.Item
|
||||
key={index}
|
||||
style={{
|
||||
backgroundColor: 'transparent',
|
||||
color: 'var(--text-light)',
|
||||
borderColor: 'var(--bg-dark)'
|
||||
}}
|
||||
className="px-0"
|
||||
>
|
||||
<div className="activity-item">
|
||||
<div className="d-flex align-items-center mb-1">
|
||||
<span className="me-2">{getActivityIcon(activity.type)}</span>
|
||||
<div className="flex-grow-1">
|
||||
{activity.content}
|
||||
</div>
|
||||
<small className="text-muted">{activity.date}</small>
|
||||
</div>
|
||||
</div>
|
||||
</ListGroup.Item>
|
||||
))}
|
||||
</ListGroup>
|
||||
</div>
|
||||
</Tab>
|
||||
|
||||
{/* Вкладка с настройками */}
|
||||
<Tab eventKey="settings" title="⚙️ Настройки">
|
||||
<div className="p-3">
|
||||
<Form>
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label>Уведомления</Form.Label>
|
||||
<Form.Check
|
||||
type="switch"
|
||||
label="Уведомления о новых комментариях"
|
||||
defaultChecked
|
||||
/>
|
||||
<Form.Check
|
||||
type="switch"
|
||||
label="Уведомления о лайках"
|
||||
defaultChecked
|
||||
/>
|
||||
<Form.Check
|
||||
type="switch"
|
||||
label="Email-рассылка"
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label>Конфиденциальность</Form.Label>
|
||||
<Form.Check
|
||||
type="switch"
|
||||
label="Скрыть профиль от поисковых систем"
|
||||
/>
|
||||
<Form.Check
|
||||
type="switch"
|
||||
label="Только подписчики могут комментировать"
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label>Тема оформления</Form.Label>
|
||||
<Form.Select>
|
||||
<option>Темная (текущая)</option>
|
||||
<option>Светлая</option>
|
||||
<option>Авто</option>
|
||||
</Form.Select>
|
||||
</Form.Group>
|
||||
|
||||
<Button variant="accent">Сохранить настройки</Button>
|
||||
</Form>
|
||||
</div>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</div>
|
||||
|
||||
{/* Модальное окно редактирования профиля */}
|
||||
<Modal show={showEditModal} onHide={() => setShowEditModal(false)} centered>
|
||||
<Modal.Header closeButton style={{ backgroundColor: 'var(--bg-medium)', color: 'var(--text-light)' }}>
|
||||
<Modal.Title>Редактировать профиль</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body style={{ backgroundColor: 'var(--bg-medium)', color: 'var(--text-light)' }}>
|
||||
<Form>
|
||||
<Form.Group className="mb-3 text-center">
|
||||
<Image
|
||||
src={user.avatar}
|
||||
roundedCircle
|
||||
style={{ width: '100px', height: '100px', objectFit: 'cover' }}
|
||||
/>
|
||||
<div className="mt-2">
|
||||
<Button variant="outline-accent" size="sm">
|
||||
Сменить аватар
|
||||
</Button>
|
||||
</div>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label>Имя и фамилия</Form.Label>
|
||||
<Form.Control
|
||||
value={editForm.name}
|
||||
onChange={(e) => setEditForm({...editForm, name: e.target.value})}
|
||||
style={{ backgroundColor: 'var(--bg-dark)', color: 'var(--text-light)' }}
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label>О себе</Form.Label>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
rows={3}
|
||||
value={editForm.bio}
|
||||
onChange={(e) => setEditForm({...editForm, bio: e.target.value})}
|
||||
style={{ backgroundColor: 'var(--bg-dark)', color: 'var(--text-light)' }}
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label>Местоположение</Form.Label>
|
||||
<Form.Control
|
||||
value={editForm.location}
|
||||
onChange={(e) => setEditForm({...editForm, location: e.target.value})}
|
||||
style={{ backgroundColor: 'var(--bg-dark)', color: 'var(--text-light)' }}
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label>Веб-сайт</Form.Label>
|
||||
<Form.Control
|
||||
value={editForm.website}
|
||||
onChange={(e) => setEditForm({...editForm, website: e.target.value})}
|
||||
style={{ backgroundColor: 'var(--bg-dark)', color: 'var(--text-light)' }}
|
||||
/>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
</Modal.Body>
|
||||
<Modal.Footer style={{ backgroundColor: 'var(--bg-medium)' }}>
|
||||
<Button variant="secondary" onClick={() => setShowEditModal(false)}>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button variant="accent" onClick={handleSaveProfile}>
|
||||
Сохранить изменения
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfilePage;
|
||||
330
online-library/src/Registration/AuthPages.jsx
Normal file
@@ -0,0 +1,330 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Container,
|
||||
Row,
|
||||
Col,
|
||||
Form,
|
||||
Button,
|
||||
Card,
|
||||
Tab,
|
||||
Tabs,
|
||||
Alert,
|
||||
} from "react-bootstrap";
|
||||
import { authAPI } from "../ApiRequest/ApiClient";
|
||||
import axios from "axios";
|
||||
import { setAccessToken } from "../ApiRequest/JwtUtils";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
const AuthPages = () => {
|
||||
const [activeTab, setActiveTab] = useState("login");
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: "var(--bg-dark)",
|
||||
minHeight: "100vh",
|
||||
padding: "20px 0",
|
||||
}}
|
||||
>
|
||||
<Container>
|
||||
<Row className="justify-content-center">
|
||||
<Col md={6} lg={5}>
|
||||
<Card
|
||||
style={{
|
||||
backgroundColor: "var(--bg-medium)",
|
||||
border: "none",
|
||||
borderRadius: "10px",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<Card.Body className="p-0">
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onSelect={(k) => setActiveTab(k)}
|
||||
fill
|
||||
style={{
|
||||
backgroundColor: "var(--bg-dark)",
|
||||
}}
|
||||
>
|
||||
<Tab eventKey="login" title="Вход">
|
||||
<LoginForm />
|
||||
</Tab>
|
||||
<Tab eventKey="register" title="Регистрация">
|
||||
<RegisterForm />
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const LoginForm = () => {
|
||||
const navigate = useNavigate();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [message, setMessage] = useState("");
|
||||
const [error, setError] = useState(null);
|
||||
const [formData, setFormData] = useState({
|
||||
username: "",
|
||||
password: "",
|
||||
});
|
||||
useEffect(()=>{console.log("GHDPFHBGFJVK;DLJHFGKDJOHGLKL")},[formData.username])
|
||||
|
||||
const handleChange = (e) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
[e.target.name]: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
// Обработка входа
|
||||
console.log("Login data:", formData);
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await authAPI.login(formData);
|
||||
if (response.status === 200) {
|
||||
const token = response.data;
|
||||
setAccessToken(token);
|
||||
setTimeout(() => {
|
||||
navigate("/");
|
||||
}, 1500);
|
||||
}
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
setError(error.response.data.message);
|
||||
} else {
|
||||
setError("Ошибка сети. Повторите попытку чуть чуть попозже");
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: "30px" }}>
|
||||
<h3 className="text-center mb-4" style={{ color: "var(--text-light)" }}>
|
||||
Вход в аккаунт
|
||||
</h3>
|
||||
{error && <Alert variant="danger">{error}</Alert>}
|
||||
{message && <Alert variant="success">{message}</Alert>}
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label style={{ color: "var(--text-light)" }}>
|
||||
Email/Логин
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
name="username"
|
||||
value={formData.username}
|
||||
onChange={handleChange}
|
||||
required
|
||||
style={{
|
||||
backgroundColor: "var(--bg-dark)",
|
||||
border: "1px solid var(--accent)",
|
||||
color: "var(--text-light)",
|
||||
}}
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group className="mb-4">
|
||||
<Form.Label style={{ color: "var(--text-light)" }}>Пароль</Form.Label>
|
||||
<Form.Control
|
||||
type="password"
|
||||
name="password"
|
||||
value={formData.password}
|
||||
onChange={handleChange}
|
||||
required
|
||||
style={{
|
||||
backgroundColor: "var(--bg-dark)",
|
||||
border: "1px solid var(--accent)",
|
||||
color: "var(--text-light)",
|
||||
}}
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<div className="d-grid gap-2">
|
||||
<Button
|
||||
type="submit"
|
||||
style={{
|
||||
backgroundColor: "var(--accent)",
|
||||
border: "none",
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
size="lg"
|
||||
>
|
||||
Войти
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const RegisterForm = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [message, setMessage] = useState("");
|
||||
const [formData, setFormData] = useState({
|
||||
username: "",
|
||||
fio: "",
|
||||
email: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
});
|
||||
|
||||
const handleChange = (e) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
[e.target.name]: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
console.log("Register data:", formData);
|
||||
setLoading(true);
|
||||
if (formData.password !== formData.confirmPassword) {
|
||||
setError("Пароли не совпадают!");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await authAPI.registration(formData);
|
||||
if (response.data) {
|
||||
setMessage("Регистрация прошла успешно!");
|
||||
setFormData({
|
||||
fio: "",
|
||||
email: "",
|
||||
username: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
});
|
||||
setTimeout(() => {}, 2000);
|
||||
} else {
|
||||
setError(response.data.message || "Ошибка регистрации");
|
||||
}
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
setError(error.response.data.message);
|
||||
} else {
|
||||
setError("Ошибка сети. Попробуйте позже.");
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: "30px" }}>
|
||||
<h3 className="text-center mb-4" style={{ color: "var(--text-light)" }}>
|
||||
Создать аккаунт
|
||||
</h3>
|
||||
{error && <Alert variant="danger">{error}</Alert>}
|
||||
{message && <Alert variant="success">{message}</Alert>}
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label style={{ color: "var(--text-light)" }}>Логин</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
name="username"
|
||||
value={formData.username}
|
||||
onChange={handleChange}
|
||||
required
|
||||
style={{
|
||||
backgroundColor: "var(--bg-dark)",
|
||||
border: "1px solid var(--accent)",
|
||||
color: "var(--text-light)",
|
||||
}}
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label style={{ color: "var(--text-light)" }}>Имя</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
name="fio"
|
||||
value={formData.fio}
|
||||
onChange={handleChange}
|
||||
required
|
||||
style={{
|
||||
backgroundColor: "var(--bg-dark)",
|
||||
border: "1px solid var(--accent)",
|
||||
color: "var(--text-light)",
|
||||
}}
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label style={{ color: "var(--text-light)" }}>Email</Form.Label>
|
||||
<Form.Control
|
||||
type="email"
|
||||
name="email"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
required
|
||||
style={{
|
||||
backgroundColor: "var(--bg-dark)",
|
||||
border: "1px solid var(--accent)",
|
||||
color: "var(--text-light)",
|
||||
}}
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label style={{ color: "var(--text-light)" }}>Пароль</Form.Label>
|
||||
<Form.Control
|
||||
type="password"
|
||||
name="password"
|
||||
value={formData.password}
|
||||
onChange={handleChange}
|
||||
required
|
||||
style={{
|
||||
backgroundColor: "var(--bg-dark)",
|
||||
border: "1px solid var(--accent)",
|
||||
color: "var(--text-light)",
|
||||
}}
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group className="mb-4">
|
||||
<Form.Label style={{ color: "var(--text-light)" }}>
|
||||
Подтвердите пароль
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
type="password"
|
||||
name="confirmPassword"
|
||||
value={formData.confirmPassword}
|
||||
onChange={handleChange}
|
||||
required
|
||||
style={{
|
||||
backgroundColor: "var(--bg-dark)",
|
||||
border: "1px solid var(--accent)",
|
||||
color: "var(--text-light)",
|
||||
}}
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<div className="d-grid gap-2">
|
||||
<Button
|
||||
type="submit"
|
||||
style={{
|
||||
backgroundColor: "var(--accent)",
|
||||
border: "none",
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
size="lg"
|
||||
>
|
||||
Зарегистрироваться
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthPages;
|
||||
64
online-library/src/Search/Search.jsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import BookCard from "../BookCard/BookCard";
|
||||
import { useEffect, useState } from "react";
|
||||
import { bookAPI } from "../ApiRequest/ApiClient";
|
||||
import Autocomplete from "@mui/material/Autocomplete";
|
||||
import TextField from "@mui/material/TextField";
|
||||
|
||||
function Search() {
|
||||
const [books, setBooks] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchBooks = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await bookAPI.getAll({ size: 100 });
|
||||
setBooks(response.data.content);
|
||||
} catch (error) {
|
||||
setError(error.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchBooks();
|
||||
}, []);
|
||||
const eventPressEnter = (event) => {
|
||||
if (event.key === "Enter") {
|
||||
const value = event.target.value;
|
||||
if (!books.includes(value)) {
|
||||
console.log("Нет такой книги");
|
||||
return;
|
||||
}
|
||||
console.log(event.target.value)
|
||||
}
|
||||
};
|
||||
if (loading) return <div> Загрузка</div>;
|
||||
if (error) return <div> Ошибка: {error}</div>;
|
||||
return (
|
||||
<main className="container">
|
||||
<div className="mb-5 mx-auto" style={{ maxWidth: "6000px" }}>
|
||||
<Autocomplete
|
||||
id="autocompleteSearch"
|
||||
freeSolo
|
||||
options={books.map((book) => book.title)}
|
||||
className="form-control form-control-lg"
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
label="Поиск книг"
|
||||
onKeyDown={eventPressEnter}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
|
||||
{books.map((book) => (
|
||||
<BookCard key={book.id} book={book}></BookCard>
|
||||
))}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default Search;
|
||||
146
online-library/src/header/Headers.jsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import { NavLink, useNavigate } from "react-router-dom";
|
||||
import { HashLink } from "react-router-hash-link";
|
||||
import { getAccessToken, removeAccessToken } from "../ApiRequest/JwtUtils";
|
||||
import { useEffect, useState } from "react";
|
||||
import axios from "axios";
|
||||
import { authAPI, bookAPI } from "../ApiRequest/ApiClient";
|
||||
import { Alert } from "react-bootstrap";
|
||||
|
||||
function Headers() {
|
||||
const [genres, setGenres] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [message, setMessage] = useState(null);
|
||||
const navigate = useNavigate();
|
||||
useEffect(() => {
|
||||
const fettchData = async () => {
|
||||
try {
|
||||
const response = await bookAPI.getBooksGenre();
|
||||
if (response.status === 200) {
|
||||
setGenres(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
setError(error.response?.data.message);
|
||||
} else {
|
||||
setError("Ошибка запроса к серверу, повторите позднее");
|
||||
}
|
||||
}
|
||||
};
|
||||
fettchData();
|
||||
}, []);
|
||||
const logout = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await authAPI.logout();
|
||||
removeAccessToken();
|
||||
if (response.status === 200) {
|
||||
setMessage("Вы успешно вышли!");
|
||||
setTimeout(() => {}, 1500);
|
||||
navigate("/");
|
||||
}
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
setError(error.response.data.message);
|
||||
} else {
|
||||
setError("Ошибка подключения");
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
const handleClick = () => {
|
||||
logout();
|
||||
};
|
||||
|
||||
return (
|
||||
<header className="mb-4">
|
||||
<h1
|
||||
className="text-center display-4 fw-bold"
|
||||
style={{ color: "var(--accent)" }}
|
||||
>
|
||||
Добро пожаловать на BookHub
|
||||
</h1>
|
||||
<h4 className="text-center mb-4">Самые скучные книги только у нас!</h4>
|
||||
{error && <Alert variant="danger">{error}</Alert>}
|
||||
{message && <Alert variant="success">{message}</Alert>}
|
||||
<nav className="navbar navbar-expand-lg navbar-custom p-3">
|
||||
<div className="container-fluid">
|
||||
<button
|
||||
className="navbar-toggler"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#navbarNav"
|
||||
>
|
||||
<span className="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div className="collapse navbar-collapse" id="navbarNav">
|
||||
<ul className="navbar-nav mx-auto gap-3">
|
||||
<li className="nav-item">
|
||||
<NavLink to="/" className="nav-link nav-link-custom">
|
||||
Главная страница
|
||||
</NavLink>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<NavLink to="/search" className="nav-link nav-link-custom">
|
||||
Поиск
|
||||
</NavLink>
|
||||
</li>
|
||||
<li className="nav-item dropdown dropdown-custom">
|
||||
<NavLink
|
||||
className="nav-link nav-link-custom dropdown-toggle"
|
||||
to="/choice"
|
||||
role="button"
|
||||
data-bs-toggle="dropdown"
|
||||
>
|
||||
Что выбрать
|
||||
</NavLink>
|
||||
<ul className="dropdown-menu">
|
||||
{genres.map((genre, index) => (
|
||||
<li>
|
||||
<HashLink
|
||||
className="dropdown-item"
|
||||
to={`/choice#${genre}`}
|
||||
key={index}
|
||||
>
|
||||
{genre}
|
||||
</HashLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
{getAccessToken() ? (
|
||||
<>
|
||||
<li className="nav-item">
|
||||
<NavLink to="/lk" className="nav-link nav-link-custom">
|
||||
Личный кабинет
|
||||
</NavLink>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<button
|
||||
className="nav-link nav-link-custom"
|
||||
onClick={handleClick}
|
||||
>
|
||||
Выйти
|
||||
</button>
|
||||
</li>
|
||||
</>
|
||||
) : (
|
||||
<li className="nav-item">
|
||||
<NavLink
|
||||
to="/registration"
|
||||
className="nav-link nav-link-custom"
|
||||
>
|
||||
Вход/Регистрация
|
||||
</NavLink>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
export default Headers;
|
||||
10
online-library/src/index.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App";
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById("root"));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
67
online-library/src/style.css
Normal file
@@ -0,0 +1,67 @@
|
||||
:root {
|
||||
--bg-dark: #222831;
|
||||
--bg-medium: #393e46;
|
||||
--accent: #00adb5;
|
||||
--text-light: #eeeeee;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-dark);
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
.navbar-custom {
|
||||
background-color: var(--accent);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.nav-link-custom {
|
||||
color: var(--bg-dark) !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.nav-link-custom:hover {
|
||||
color: var(--text-light) !important;
|
||||
}
|
||||
|
||||
.dropdown-custom .dropdown-menu {
|
||||
background-color: var(--bg-medium);
|
||||
}
|
||||
|
||||
.dropdown-custom .dropdown-item {
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
.dropdown-custom .dropdown-item:hover {
|
||||
background-color: var(--accent);
|
||||
color: var(--bg-dark);
|
||||
}
|
||||
|
||||
.form-custom input {
|
||||
background-color: var(--bg-medium);
|
||||
color: var(--text-light);
|
||||
border: 1px solid var(--accent);
|
||||
}
|
||||
|
||||
.btn-custom {
|
||||
background-color: var(--accent);
|
||||
color: var(--bg-dark);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link {
|
||||
color: var(--text-light);
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link.active {
|
||||
color: var(--accent);
|
||||
background-color: var(--bg-medium);
|
||||
border: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.nav-tabs {
|
||||
border-bottom: 1px solid var(--accent);
|
||||
}
|
||||
24
package-lock.json
generated
@@ -2953,18 +2953,6 @@
|
||||
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/json5": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
|
||||
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.0"
|
||||
},
|
||||
"bin": {
|
||||
"json5": "lib/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
@@ -4325,6 +4313,18 @@
|
||||
"strip-bom": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tsconfig-paths/node_modules/json5": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
|
||||
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.0"
|
||||
},
|
||||
"bin": {
|
||||
"json5": "lib/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
|
||||
35
package.json
@@ -3,26 +3,27 @@
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
"build": "vite build",
|
||||
"serve": "http-server -p 3000 ./dist/",
|
||||
"prod": "npm-run-all build serve",
|
||||
"lint": "eslint . --ext js --report-unused-disable-directives --max-warnings 0"
|
||||
"start": "npm-run-all --parallel backend vite",
|
||||
"vite": "vite",
|
||||
"build": "vite build",
|
||||
"serve": "http-server -p 3000 ./dist/",
|
||||
"backend":"json-server database/db.json -p 5174",
|
||||
"prod": "npm-run-all build --parallel backend serve",
|
||||
"lint": "eslint . --ext js --report-unused-disable-directives --max-warnings 0"
|
||||
},
|
||||
"dependencies": {
|
||||
"bootstrap": "5.3.3",
|
||||
"bootstrap-icons": "1.11.3"
|
||||
"bootstrap": "5.3.3",
|
||||
"bootstrap-icons": "1.11.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"http-server": "14.1.1",
|
||||
"vite": "6.2.0",
|
||||
"npm-run-all": "4.1.5",
|
||||
"eslint": "8.56.0",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-prettier": "10.0.2",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-prettier": "5.2.3",
|
||||
"eslint-plugin-html": "8.1.2"
|
||||
"eslint": "8.56.0",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-prettier": "10.0.2",
|
||||
"eslint-plugin-html": "8.1.2",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-prettier": "5.2.3",
|
||||
"http-server": "14.1.1",
|
||||
"npm-run-all": "4.1.5",
|
||||
"vite": "6.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
BIN
res/1984.jpg
Normal file
|
After Width: | Height: | Size: 123 KiB |
BIN
res/451 по фарингейту.jpg
Normal file
|
After Width: | Height: | Size: 119 KiB |
1
res/telegram.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" width="50px" height="50px"><path d="M 25 2 C 12.309288 2 2 12.309297 2 25 C 2 37.690703 12.309288 48 25 48 C 37.690712 48 48 37.690703 48 25 C 48 12.309297 37.690712 2 25 2 z M 25 4 C 36.609833 4 46 13.390175 46 25 C 46 36.609825 36.609833 46 25 46 C 13.390167 46 4 36.609825 4 25 C 4 13.390175 13.390167 4 25 4 z M 34.087891 14.035156 C 33.403891 14.035156 32.635328 14.193578 31.736328 14.517578 C 30.340328 15.020578 13.920734 21.992156 12.052734 22.785156 C 10.984734 23.239156 8.9960938 24.083656 8.9960938 26.097656 C 8.9960938 27.432656 9.7783594 28.3875 11.318359 28.9375 C 12.146359 29.2325 14.112906 29.828578 15.253906 30.142578 C 15.737906 30.275578 16.25225 30.34375 16.78125 30.34375 C 17.81625 30.34375 18.857828 30.085859 19.673828 29.630859 C 19.666828 29.798859 19.671406 29.968672 19.691406 30.138672 C 19.814406 31.188672 20.461875 32.17625 21.421875 32.78125 C 22.049875 33.17725 27.179312 36.614156 27.945312 37.160156 C 29.021313 37.929156 30.210813 38.335938 31.382812 38.335938 C 33.622813 38.335938 34.374328 36.023109 34.736328 34.912109 C 35.261328 33.299109 37.227219 20.182141 37.449219 17.869141 C 37.600219 16.284141 36.939641 14.978953 35.681641 14.376953 C 35.210641 14.149953 34.672891 14.035156 34.087891 14.035156 z M 34.087891 16.035156 C 34.362891 16.035156 34.608406 16.080641 34.816406 16.181641 C 35.289406 16.408641 35.530031 16.914688 35.457031 17.679688 C 35.215031 20.202687 33.253938 33.008969 32.835938 34.292969 C 32.477938 35.390969 32.100813 36.335938 31.382812 36.335938 C 30.664813 36.335938 29.880422 36.08425 29.107422 35.53125 C 28.334422 34.97925 23.201281 31.536891 22.488281 31.087891 C 21.863281 30.693891 21.201813 29.711719 22.132812 28.761719 C 22.899812 27.979719 28.717844 22.332938 29.214844 21.835938 C 29.584844 21.464938 29.411828 21.017578 29.048828 21.017578 C 28.923828 21.017578 28.774141 21.070266 28.619141 21.197266 C 28.011141 21.694266 19.534781 27.366266 18.800781 27.822266 C 18.314781 28.124266 17.56225 28.341797 16.78125 28.341797 C 16.44825 28.341797 16.111109 28.301891 15.787109 28.212891 C 14.659109 27.901891 12.750187 27.322734 11.992188 27.052734 C 11.263188 26.792734 10.998047 26.543656 10.998047 26.097656 C 10.998047 25.463656 11.892938 25.026 12.835938 24.625 C 13.831938 24.202 31.066062 16.883437 32.414062 16.398438 C 33.038062 16.172438 33.608891 16.035156 34.087891 16.035156 z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
1
res/vk.svg
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
res/мы.jpg
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
res/о дивный новый мир.jpg
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
res/повелитель мух .jpg
Normal file
|
After Width: | Height: | Size: 33 KiB |