16 Commits

Author SHA1 Message Date
d7f289234c Отображение тэгов 2025-11-17 13:18:16 +04:00
294699289c Почему-то при попытке убрать импорт бутстрапа в footer задний фон пропадает.... Мега странно 2025-10-04 11:14:23 +04:00
10ddff97d0 Убрал лишние импорты 2025-10-04 10:48:39 +04:00
f347eb098b Ура, все работаетgit add .git add . НАЧИНАЕТСЯ НОВАЯ ЭРА 2025-09-25 09:41:23 +04:00
e378591189 Осталось настроить поиск до конца и все, вроде как фронт полностью на данном этапе готов)) 2025-09-25 00:39:08 +04:00
546f09fda3 Сейчас будем пытаться сделать все максимально гибко 2025-09-24 11:54:07 +04:00
51e33fd62d Теперь есть возможность регистрации и авторизации. Осталось настроить выход и автопродление токена)) 2025-09-20 23:51:02 +04:00
ba1ad50382 Сделали главное - получение всех книг и отображение книг по одной с главной страницы 2025-09-20 01:41:45 +04:00
cc60f5808a Хрен его знает, что сделал, но теперь должно работать 2025-09-06 17:02:18 +04:00
da02613094 Удалил старый html. Теперь в планах подключить бэк, а уже после думать дальше 2025-09-06 16:40:00 +04:00
07a092624b Основная логика работает 2025-09-06 16:38:59 +04:00
33f1f364b5 Gg wp 2025-09-06 01:19:31 +04:00
f6de886c74 Чето очень большой коммит, если честно)) 2025-09-06 00:34:47 +04:00
6006095ad7 Создал бд и настроил одновременный запуск бд и приложения 2025-05-05 19:25:24 +04:00
077e8e59fc Разложил все по папочкам) 2025-05-05 18:53:25 +04:00
fe9c162596 Переложил все изображения в папку res 2025-05-05 17:43:16 +04:00
69 changed files with 20138 additions and 128 deletions

83
.idea/workspace.xml generated Normal file
View 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">{
&quot;customColor&quot;: &quot;&quot;,
&quot;associatedIndex&quot;: 5
}</component>
<component name="ProjectId" id="32ugu54CHfH6I1mAvywPuSyNpUc" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;ModuleVcsDetector.initialDetectionPerformed&quot;: &quot;true&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,
&quot;git-widget-placeholder&quot;: &quot;lab__work4&quot;,
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
&quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
&quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
&quot;npm.start.executor&quot;: &quot;Run&quot;,
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
}
}</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
View 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
}
]
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
View File

@@ -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",

View File

@@ -1,8 +1,8 @@
{
"hash": "a4354f03",
"hash": "f196cd23",
"configHash": "f1521633",
"lockfileHash": "35a58dc0",
"browserHash": "41763e6c",
"lockfileHash": "c6b67ffc",
"browserHash": "fa914368",
"optimized": {},
"chunks": {}
}

23
online-library/.gitignore vendored Normal file
View 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
View 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

File diff suppressed because it is too large Load Diff

View 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"
]
}
}

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

View File

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 123 KiB

View File

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 119 KiB

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 81 KiB

View File

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

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

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

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

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

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

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

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

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

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

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

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

View File

@@ -0,0 +1,3 @@
.footer-custom {
background-color: var(--bg-medium);
}

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -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",

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

1
res/telegram.svg Normal file
View 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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.4 KiB

BIN
res/мы.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB