add wisiwig editor TinyMCE. default content type = markdown.

This commit is contained in:
mirivlad 2025-11-24 11:53:56 +08:00
parent d97e4d4944
commit d7fe90a615
27 changed files with 5676 additions and 5055 deletions

174
README.md
View File

@ -1,88 +1,88 @@
# Web Writer # Web Writer
**Лицензия:** AGPLv3 **Лицензия:** AGPLv3
**Web Writer** — веб-приложение для написания, хранения и публикации книг и серий книг. Поддерживает Markdown с расширением для диалогов, автосохранение текста, экспорт и управление пользователями. **Web Writer** — веб-приложение для написания, хранения и публикации книг и серий книг. Поддерживает Markdown с расширением для диалогов, автосохранение текста, экспорт и управление пользователями.
--- ---
## 🚀 Возможности ## 🚀 Возможности
- **Книги и серии:** создавайте серии и добавляйте книги с главами. - **Книги и серии:** создавайте серии и добавляйте книги с главами.
- **Редактор книг:** Markdown, автосохранение текста, интерактивное содержание. - **Редактор книг:** Markdown, автосохранение текста, интерактивное содержание.
- **Предпросмотр книг:** - **Предпросмотр книг:**
- **Автор:** видит все черновики и опубликованные главы. - **Автор:** видит все черновики и опубликованные главы.
- **Публичный доступ:** только опубликованные главы по ссылке с `shared_token`. - **Публичный доступ:** только опубликованные главы по ссылке с `shared_token`.
- **Обложки и аватары:** добавляйте изображения к книгам и профилям. - **Обложки и аватары:** добавляйте изображения к книгам и профилям.
- **Экспорт:** PDF, DOCX, HTML, TXT. - **Экспорт:** PDF, DOCX, HTML, TXT.
- **Администрирование пользователей:** - **Администрирование пользователей:**
- Управление аккаунтами, активация/деактивация. - Управление аккаунтами, активация/деактивация.
- При удалении пользователя удаляются все его книги. - При удалении пользователя удаляются все его книги.
- **Публичные ссылки:** делитесь `shared_token` для просмотра опубликованных глав. - **Публичные ссылки:** делитесь `shared_token` для просмотра опубликованных глав.
--- ---
## ⚙️ Требования ## ⚙️ Требования
- **PHP:** 8.0 и выше - **PHP:** 8.0 и выше
- **MySQL** с InnoDB и внешними ключами - **MySQL** с InnoDB и внешними ключами
- **PHP расширения:** `mbstring`, `json`, `PDO` - **PHP расширения:** `mbstring`, `json`, `PDO`
- Веб-сервер с правами на запись в папки `config/` и `uploads/` - Веб-сервер с правами на запись в папки `config/` и `uploads/`
> Все библиотеки уже включены в `vendor/`. Composer не нужен. > Все библиотеки уже включены в `vendor/`. Composer не нужен.
--- ---
## 🛠 Установка ## 🛠 Установка
1. Скопируйте файлы на веб-сервер. 1. Скопируйте файлы на веб-сервер.
2. Проверьте доступность папок `config/` и `uploads/` для записи. 2. Проверьте доступность папок `config/` и `uploads/` для записи.
3. Перейдите в браузере на `install.php` и следуйте шагам: 3. Перейдите в браузере на `install.php` и следуйте шагам:
**Шаг 1: Настройки базы данных** **Шаг 1: Настройки базы данных**
- Хост БД - Хост БД
- Имя базы данных - Имя базы данных
- Пользователь и пароль - Пользователь и пароль
**Шаг 2: Создание администратора** **Шаг 2: Создание администратора**
- Имя пользователя - Имя пользователя
- Пароль - Пароль
- Email (по желанию) - Email (по желанию)
- Отображаемое имя (по желанию) - Отображаемое имя (по желанию)
4. После успешной установки файл `config/config.php` будет сгенерирован автоматически. 4. После успешной установки файл `config/config.php` будет сгенерирован автоматически.
5. Перейдите на главную страницу приложения (`index.php`) и войдите под админом. 5. Перейдите на главную страницу приложения (`index.php`) и войдите под админом.
6. **Не забудьте удалить или переместить файл install.php!!!** 6. **Не забудьте удалить или переместить файл install.php!!!**
--- ---
## 📝 Конфигурация ## 📝 Конфигурация
Файл `config/config.php` содержит: Файл `config/config.php` содержит:
- Подключение к базе данных: `DB_HOST`, `DB_USER`, `DB_PASS`, `DB_NAME` - Подключение к базе данных: `DB_HOST`, `DB_USER`, `DB_PASS`, `DB_NAME`
- Пути к файлам: - Пути к файлам:
- `UPLOAD_PATH` — корневая папка загрузок - `UPLOAD_PATH` — корневая папка загрузок
- `COVERS_PATH` / `COVERS_URL` — обложки книг - `COVERS_PATH` / `COVERS_URL` — обложки книг
- `AVATARS_PATH` / `AVATARS_URL` — аватары пользователей - `AVATARS_PATH` / `AVATARS_URL` — аватары пользователей
- Адрес сайта: `SITE_URL` - Адрес сайта: `SITE_URL`
- Имя приложения: `APP_NAME` = "Web Writer" - Имя приложения: `APP_NAME` = "Web Writer"
--- ---
## 🛠 Дальнейшее развитие ## 🛠 Дальнейшее развитие
- Планирую вынести работу с сущностями (книги, главы, серии, пользователи) в контроллеры. - Планирую вынести работу с сущностями (книги, главы, серии, пользователи) в контроллеры.
- Создать единую точку входа для приложения. - Создать единую точку входа для приложения.
--- ---
## ❗ Поддержка ## ❗ Поддержка
Все ошибки и предложения шлите в issue Все ошибки и предложения шлите в issue
--- ---
## 📜 Лицензия ## 📜 Лицензия
Приложение распространяется под лицензией [AGPLv3](https://www.gnu.org/licenses/agpl-3.0.html). Приложение распространяется под лицензией [AGPLv3](https://www.gnu.org/licenses/agpl-3.0.html).

File diff suppressed because it is too large Load Diff

View File

@ -1,192 +1,192 @@
<?php <?php
require_once 'config/config.php'; require_once 'config/config.php';
require_once 'includes/parsedown/ParsedownExtra.php'; require_once 'includes/parsedown/ParsedownExtra.php';
$Parsedown = new ParsedownExtra(); $Parsedown = new ParsedownExtra();
$author_id = (int)($_GET['id'] ?? 0); $author_id = (int)($_GET['id'] ?? 0);
if (!$author_id) { if (!$author_id) {
http_response_code(400); http_response_code(400);
echo "<h2>Неверный запрос</h2>"; echo "<h2>Неверный запрос</h2>";
include 'views/footer.php'; include 'views/footer.php';
exit; exit;
} }
$stmt = $pdo->prepare("SELECT id, username, display_name, avatar, bio FROM users WHERE id = ?"); $stmt = $pdo->prepare("SELECT id, username, display_name, avatar, bio FROM users WHERE id = ?");
$stmt->execute([$author_id]); $stmt->execute([$author_id]);
$author = $stmt->fetch(PDO::FETCH_ASSOC); $author = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$author) { if (!$author) {
http_response_code(404); http_response_code(404);
echo "<h2>Автор не найден</h2>"; echo "<h2>Автор не найден</h2>";
include 'views/footer.php'; include 'views/footer.php';
exit; exit;
} }
$bookModel = new Book($pdo); $bookModel = new Book($pdo);
$books = $bookModel->findByUser($author_id, true); // только опубликованные $books = $bookModel->findByUser($author_id, true); // только опубликованные
// Получаем статистику автора // Получаем статистику автора
$total_books = count($books); $total_books = count($books);
$total_words = 0; $total_words = 0;
$total_chapters = 0; $total_chapters = 0;
foreach ($books as $book) { foreach ($books as $book) {
$book_stats = $bookModel->getBookStats($book['id'], true); $book_stats = $bookModel->getBookStats($book['id'], true);
$total_words += $book_stats['total_words'] ?? 0; $total_words += $book_stats['total_words'] ?? 0;
$total_chapters += $book_stats['chapter_count'] ?? 0; $total_chapters += $book_stats['chapter_count'] ?? 0;
} }
$page_title = ($author['display_name'] ?: $author['username']) . ' — публичная страница'; $page_title = ($author['display_name'] ?: $author['username']) . ' — публичная страница';
include 'views/header.php'; include 'views/header.php';
?> ?>
<div class="container"> <div class="container">
<article style="max-width: 800px; margin: 0 auto;"> <article style="max-width: 800px; margin: 0 auto;">
<header style="text-align: center; margin-bottom: 2rem; border-bottom: 2px solid #eee; padding-bottom: 1rem;"> <header style="text-align: center; margin-bottom: 2rem; border-bottom: 2px solid #eee; padding-bottom: 1rem;">
<!-- Аватарка автора --> <!-- Аватарка автора -->
<div style="margin-bottom: 1rem;"> <div style="margin-bottom: 1rem;">
<?php if (!empty($author['avatar'])): ?> <?php if (!empty($author['avatar'])): ?>
<img src="<?= AVATARS_URL . e($author['avatar']) ?>" <img src="<?= AVATARS_URL . e($author['avatar']) ?>"
alt="<?= e($author['display_name'] ?: $author['username']) ?>" alt="<?= e($author['display_name'] ?: $author['username']) ?>"
style="width: 150px; height: 150px; border-radius: 50%; border: 3px solid #007bff; object-fit: cover;" style="width: 150px; height: 150px; border-radius: 50%; border: 3px solid #007bff; object-fit: cover;"
onerror="this.style.display='none'"> onerror="this.style.display='none'">
<?php else: ?> <?php else: ?>
<div style="width: 150px; height: 150px; border-radius: 50%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; align-items: center; justify-content: center; color: white; font-size: 3rem; margin: 0 auto;"> <div style="width: 150px; height: 150px; border-radius: 50%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; align-items: center; justify-content: center; color: white; font-size: 3rem; margin: 0 auto;">
<?= mb_substr(e($author['display_name'] ?: $author['username']), 0, 1) ?> <?= mb_substr(e($author['display_name'] ?: $author['username']), 0, 1) ?>
</div> </div>
<?php endif; ?> <?php endif; ?>
</div> </div>
<h1 style="margin-bottom: 0.5rem;"><?= e($author['display_name'] ?: $author['username']) ?></h1> <h1 style="margin-bottom: 0.5rem;"><?= e($author['display_name'] ?: $author['username']) ?></h1>
<!-- Биография автора --> <!-- Биография автора -->
<?php if (!empty($author['bio'])): ?> <?php if (!empty($author['bio'])): ?>
<div style="background: #f8f9fa; padding: 1.5rem; border-radius: 8px; margin: 1rem 0; text-align: left;"> <div style="background: #f8f9fa; padding: 1.5rem; border-radius: 8px; margin: 1rem 0; text-align: left;">
<?= $Parsedown->text($author['bio']) ?> <?= $Parsedown->text($author['bio']) ?>
</div> </div>
<?php endif; ?> <?php endif; ?>
<!-- Статистика автора --> <!-- Статистика автора -->
<div style="display: flex; justify-content: center; gap: 2rem; flex-wrap: wrap; font-size: 0.9em; color: #666;"> <div style="display: flex; justify-content: center; gap: 2rem; flex-wrap: wrap; font-size: 0.9em; color: #666;">
<div style="text-align: center;"> <div style="text-align: center;">
<div style="font-size: 1.5em; font-weight: bold; color: #007bff;"><?= $total_books ?></div> <div style="font-size: 1.5em; font-weight: bold; color: #007bff;"><?= $total_books ?></div>
<div>Книг</div> <div>Книг</div>
</div> </div>
<div style="text-align: center;"> <div style="text-align: center;">
<div style="font-size: 1.5em; font-weight: bold; color: #28a745;"><?= $total_chapters ?></div> <div style="font-size: 1.5em; font-weight: bold; color: #28a745;"><?= $total_chapters ?></div>
<div>Глав</div> <div>Глав</div>
</div> </div>
<div style="text-align: center;"> <div style="text-align: center;">
<div style="font-size: 1.5em; font-weight: bold; color: #6f42c1;"><?= $total_words ?></div> <div style="font-size: 1.5em; font-weight: bold; color: #6f42c1;"><?= $total_words ?></div>
<div>Слов</div> <div>Слов</div>
</div> </div>
</div> </div>
</header> </header>
<h2 style="text-align: center; margin-bottom: 2rem;">Публикации автора</h2> <h2 style="text-align: center; margin-bottom: 2rem;">Публикации автора</h2>
<?php if (empty($books)): ?> <?php if (empty($books)): ?>
<div style="text-align: center; padding: 3rem; background: #f9f9f9; border-radius: 5px;"> <div style="text-align: center; padding: 3rem; background: #f9f9f9; border-radius: 5px;">
<h3>У этого автора пока нет опубликованных книг</h3> <h3>У этого автора пока нет опубликованных книг</h3>
<p>Следите за обновлениями, скоро здесь появятся новые произведения!</p> <p>Следите за обновлениями, скоро здесь появятся новые произведения!</p>
</div> </div>
<?php else: ?> <?php else: ?>
<div class="author-books"> <div class="author-books">
<?php foreach ($books as $book): ?> <?php foreach ($books as $book): ?>
<article style="display: flex; gap: 1rem; align-items: flex-start; margin-bottom: 2rem; padding: 1.5rem; background: #f8f9fa; border-radius: 8px;"> <article style="display: flex; gap: 1rem; align-items: flex-start; margin-bottom: 2rem; padding: 1.5rem; background: #f8f9fa; border-radius: 8px;">
<?php if ($book['cover_image']): ?> <?php if ($book['cover_image']): ?>
<div style="flex-shrink: 0;"> <div style="flex-shrink: 0;">
<img src="<?= COVERS_URL . e($book['cover_image']) ?>" <img src="<?= COVERS_URL . e($book['cover_image']) ?>"
alt="<?= e($book['title']) ?>" alt="<?= e($book['title']) ?>"
style="max-width: 120px; height: auto; border-radius: 4px; border: 1px solid #ddd;" style="max-width: 120px; height: auto; border-radius: 4px; border: 1px solid #ddd;"
onerror="this.style.display='none'"> onerror="this.style.display='none'">
</div> </div>
<?php else: ?> <?php else: ?>
<div style="flex-shrink: 0;"> <div style="flex-shrink: 0;">
<div class="cover-placeholder" style="width: 120px; height: 160px;">📚</div> <div class="cover-placeholder" style="width: 120px; height: 160px;">📚</div>
</div> </div>
<?php endif; ?> <?php endif; ?>
<div style="flex: 1;"> <div style="flex: 1;">
<h3 style="margin-top: 0;"><?= e($book['title']) ?></h3> <h3 style="margin-top: 0;"><?= e($book['title']) ?></h3>
<?php if ($book['genre']): ?> <?php if ($book['genre']): ?>
<p style="color: #666; margin: 0.5rem 0;"><em><?= e($book['genre']) ?></em></p> <p style="color: #666; margin: 0.5rem 0;"><em><?= e($book['genre']) ?></em></p>
<?php endif; ?> <?php endif; ?>
<?php if ($book['description']): ?> <?php if ($book['description']): ?>
<p style="margin-bottom: 1rem;"><?= nl2br(e($book['description'])) ?></p> <p style="margin-bottom: 1rem;"><?= nl2br(e($book['description'])) ?></p>
<?php endif; ?> <?php endif; ?>
<?php <?php
$book_stats = $bookModel->getBookStats($book['id'], true); $book_stats = $bookModel->getBookStats($book['id'], true);
$chapter_count = $book_stats['chapter_count'] ?? 0; $chapter_count = $book_stats['chapter_count'] ?? 0;
$word_count = $book_stats['total_words'] ?? 0; $word_count = $book_stats['total_words'] ?? 0;
?> ?>
<div style="display: flex; gap: 10px; flex-wrap: wrap; align-items: center;"> <div style="display: flex; gap: 10px; flex-wrap: wrap; align-items: center;">
<a href="view_book.php?share_token=<?= e($book['share_token']) ?>" class="adaptive-button"> <a href="view_book.php?share_token=<?= e($book['share_token']) ?>" class="adaptive-button">
Читать книгу Читать книгу
</a> </a>
<small style="color: #666;"> <small style="color: #666;">
Глав: <?= $chapter_count ?> | Слов: <?= $word_count ?> Глав: <?= $chapter_count ?> | Слов: <?= $word_count ?>
</small> </small>
</div> </div>
</div> </div>
</article> </article>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>
<?php endif; ?> <?php endif; ?>
<footer style="margin-top: 3rem; padding-top: 1rem; border-top: 2px solid #eee; text-align: center;"> <footer style="margin-top: 3rem; padding-top: 1rem; border-top: 2px solid #eee; text-align: center;">
<p style="color: #666;"> <p style="color: #666;">
Страница автора создана в <?= e(APP_NAME) ?> Страница автора создана в <?= e(APP_NAME) ?>
<?= date('Y') ?> <?= date('Y') ?>
</p> </p>
</footer> </footer>
</article> </article>
</div> </div>
<style> <style>
.author-books article { .author-books article {
transition: transform 0.2s ease, box-shadow 0.2s ease; transition: transform 0.2s ease, box-shadow 0.2s ease;
border: 1px solid #e0e0e0; border: 1px solid #e0e0e0;
} }
.author-books article:hover { .author-books article:hover {
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1); box-shadow: 0 4px 12px rgba(0,0,0,0.1);
} }
.cover-placeholder { .cover-placeholder {
width: 120px; width: 120px;
height: 160px; height: 160px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 4px; border-radius: 4px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: white; color: white;
font-size: 2rem; font-size: 2rem;
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.author-books article { .author-books article {
flex-direction: column; flex-direction: column;
text-align: center; text-align: center;
} }
.author-books .book-cover { .author-books .book-cover {
align-self: center; align-self: center;
} }
header .author-stats { header .author-stats {
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 1rem;
} }
} }
</style> </style>
<?php include 'views/footer.php'; ?> <?php include 'views/footer.php'; ?>

View File

@ -1,47 +1,47 @@
<?php <?php
require_once 'config/config.php'; require_once 'config/config.php';
require_login(); require_login();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
$_SESSION['error'] = "Неверный метод запроса"; $_SESSION['error'] = "Неверный метод запроса";
redirect('books.php'); redirect('books.php');
} }
if (!verify_csrf_token($_POST['csrf_token'] ?? '')) { if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
$_SESSION['error'] = "Ошибка безопасности"; $_SESSION['error'] = "Ошибка безопасности";
redirect('books.php'); redirect('books.php');
} }
$book_id = $_POST['book_id'] ?? null; $book_id = $_POST['book_id'] ?? null;
$user_id = $_SESSION['user_id']; $user_id = $_SESSION['user_id'];
if (!$book_id) { if (!$book_id) {
$_SESSION['error'] = "Не указана книга для удаления"; $_SESSION['error'] = "Не указана книга для удаления";
redirect('books.php'); redirect('books.php');
} }
$bookModel = new Book($pdo); $bookModel = new Book($pdo);
// Проверяем права доступа // Проверяем права доступа
if (!$bookModel->userOwnsBook($book_id, $user_id)) { if (!$bookModel->userOwnsBook($book_id, $user_id)) {
$_SESSION['error'] = "У вас нет доступа к этой книге"; $_SESSION['error'] = "У вас нет доступа к этой книге";
redirect('books.php'); redirect('books.php');
} }
// Получаем информацию о книге перед удалением // Получаем информацию о книге перед удалением
$book = $bookModel->findById($book_id); $book = $bookModel->findById($book_id);
if (!empty($book['cover_image'])) { if (!empty($book['cover_image'])) {
$cover_path = COVERS_PATH . $book['cover_image']; $cover_path = COVERS_PATH . $book['cover_image'];
if (file_exists($cover_path)) { if (file_exists($cover_path)) {
unlink($cover_path); unlink($cover_path);
} }
} }
// Удаляем книгу // Удаляем книгу
if ($bookModel->delete($book_id, $user_id)) { if ($bookModel->delete($book_id, $user_id)) {
$_SESSION['success'] = "Книга «" . e($book['title']) . "» успешно удалена"; $_SESSION['success'] = "Книга «" . e($book['title']) . "» успешно удалена";
} else { } else {
$_SESSION['error'] = "Ошибка при удалении книги"; $_SESSION['error'] = "Ошибка при удалении книги";
} }
redirect('books.php'); redirect('books.php');
?> ?>

View File

@ -1,45 +1,45 @@
<?php <?php
require_once 'config/config.php'; require_once 'config/config.php';
require_login(); require_login();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
$_SESSION['error'] = "Неверный метод запроса"; $_SESSION['error'] = "Неверный метод запроса";
redirect('books.php'); redirect('books.php');
} }
if (!verify_csrf_token($_POST['csrf_token'] ?? '')) { if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
$_SESSION['error'] = "Ошибка безопасности"; $_SESSION['error'] = "Ошибка безопасности";
redirect('books.php'); redirect('books.php');
} }
$user_id = $_SESSION['user_id']; $user_id = $_SESSION['user_id'];
$bookModel = new Book($pdo); $bookModel = new Book($pdo);
// Получаем все книги пользователя // Получаем все книги пользователя
$books = $bookModel->findByUser($user_id); $books = $bookModel->findByUser($user_id);
if (empty($books)) { if (empty($books)) {
$_SESSION['error'] = "У вас нет книг для удаления"; $_SESSION['error'] = "У вас нет книг для удаления";
redirect('books.php'); redirect('books.php');
} }
$deleted_count = 0; $deleted_count = 0;
$error_count = 0; $error_count = 0;
// Удаляем каждую книгу // Удаляем каждую книгу
foreach ($books as $book) { foreach ($books as $book) {
if ($bookModel->delete($book['id'], $user_id)) { if ($bookModel->delete($book['id'], $user_id)) {
$deleted_count++; $deleted_count++;
} else { } else {
$error_count++; $error_count++;
} }
} }
if ($error_count === 0) { if ($error_count === 0) {
$_SESSION['success'] = "Все книги успешно удалены ($deleted_count книг)"; $_SESSION['success'] = "Все книги успешно удалены ($deleted_count книг)";
} else { } else {
$_SESSION['error'] = "Удалено $deleted_count книг, не удалось удалить $error_count книг"; $_SESSION['error'] = "Удалено $deleted_count книг, не удалось удалить $error_count книг";
} }
redirect('books.php'); redirect('books.php');
?> ?>

View File

@ -1,346 +1,413 @@
<?php <?php
require_once 'config/config.php'; require_once 'config/config.php';
require_login(); require_login();
$user_id = $_SESSION['user_id']; $user_id = $_SESSION['user_id'];
$bookModel = new Book($pdo); $bookModel = new Book($pdo);
// Проверяем, редактируем ли существующую книгу // Проверяем, редактируем ли существующую книгу
$book_id = $_GET['id'] ?? null; $book_id = $_GET['id'] ?? null;
$book = null; $book = null;
$is_edit = false; $is_edit = false;
if ($book_id) { if ($book_id) {
$book = $bookModel->findById($book_id); $book = $bookModel->findById($book_id);
if (!$book || $book['user_id'] != $user_id) { if (!$book || $book['user_id'] != $user_id) {
$_SESSION['error'] = "Книга не найдена или у вас нет доступа"; $_SESSION['error'] = "Книга не найдена или у вас нет доступа";
redirect('books.php'); redirect('books.php');
} }
$is_edit = true; $is_edit = true;
} }
// Обработка формы // Обработка формы
$cover_error = ''; $cover_error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!verify_csrf_token($_POST['csrf_token'] ?? '')) { if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
$_SESSION['error'] = "Ошибка безопасности"; $_SESSION['error'] = "Ошибка безопасности";
redirect($is_edit ? "book_edit.php?id=$book_id" : 'book_edit.php'); redirect($is_edit ? "book_edit.php?id=$book_id" : 'book_edit.php');
} }
$title = trim($_POST['title'] ?? ''); $title = trim($_POST['title'] ?? '');
$description = trim($_POST['description'] ?? ''); $description = trim($_POST['description'] ?? '');
$genre = trim($_POST['genre'] ?? ''); $genre = trim($_POST['genre'] ?? '');
$editor_type = $_POST['editor_type'] ?? 'markdown';
if (empty($title)) {
$_SESSION['error'] = "Название книги обязательно"; if (empty($title)) {
} else { $_SESSION['error'] = "Название книги обязательно";
$series_id = !empty($_POST['series_id']) ? (int)$_POST['series_id'] : null; } else {
$sort_order_in_series = !empty($_POST['sort_order_in_series']) ? (int)$_POST['sort_order_in_series'] : null; $series_id = !empty($_POST['series_id']) ? (int)$_POST['series_id'] : null;
$sort_order_in_series = !empty($_POST['sort_order_in_series']) ? (int)$_POST['sort_order_in_series'] : null;
// Если серия указана, но порядок нет - генерируем автоматически
if ($series_id && !$sort_order_in_series) { if ($series_id && !$sort_order_in_series) {
$seriesModel = new Series($pdo); $seriesModel = new Series($pdo);
$sort_order_in_series = $seriesModel->getNextSortOrder($series_id); $sort_order_in_series = $seriesModel->getNextSortOrder($series_id);
} }
$data = [ $data = [
'title' => $title, 'title' => $title,
'description' => $description, 'description' => $description,
'genre' => $genre, 'genre' => $genre,
'user_id' => $user_id, 'user_id' => $user_id,
'series_id' => $series_id, 'series_id' => $series_id,
'sort_order_in_series' => $sort_order_in_series 'sort_order_in_series' => $sort_order_in_series,
]; 'editor_type' => $editor_type
$data['published'] = isset($_POST['published']) ? 1 : 0; ];
$data['published'] = isset($_POST['published']) ? 1 : 0;
// Обработка загрузки обложки
if (isset($_FILES['cover_image']) && $_FILES['cover_image']['error'] === UPLOAD_ERR_OK) { // Проверяем, изменился ли тип редактора
$cover_result = handleCoverUpload($_FILES['cover_image'], $book_id); $editor_changed = false;
if ($cover_result['success']) { $old_editor_type = null;
$bookModel->updateCover($book_id, $cover_result['filename']);
// Обновляем данные книги if ($is_edit && $book['editor_type'] !== $editor_type) {
$book = $bookModel->findById($book_id); $editor_changed = true;
} else { $old_editor_type = $book['editor_type'];
$cover_error = $cover_result['error']; }
} // Обработка загрузки обложки
} if (isset($_FILES['cover_image']) && $_FILES['cover_image']['error'] === UPLOAD_ERR_OK) {
$cover_result = handleCoverUpload($_FILES['cover_image'], $book_id);
// Обработка удаления обложки if ($cover_result['success']) {
if (isset($_POST['delete_cover']) && $_POST['delete_cover'] == '1') { $bookModel->updateCover($book_id, $cover_result['filename']);
$bookModel->deleteCover($book_id); // Обновляем данные книги
$book = $bookModel->findById($book_id); $book = $bookModel->findById($book_id);
} } else {
$cover_error = $cover_result['error'];
if ($is_edit) { }
$success = $bookModel->update($book_id, $data); }
$message = $success ? "Книга успешно обновлена" : "Ошибка при обновлении книги";
} else { // Обработка удаления обложки
$success = $bookModel->create($data); if (isset($_POST['delete_cover']) && $_POST['delete_cover'] == '1') {
$message = $success ? "Книга успешно создана" : "Ошибка при создании книги"; $bookModel->deleteCover($book_id);
$book = $bookModel->findById($book_id);
if ($success) { }
$new_book_id = $pdo->lastInsertId();
redirect("book_edit.php?id=$new_book_id"); if ($is_edit) {
} $success = $bookModel->update($book_id, $data);
}
// Конвертируем контент глав, если изменился редактор
if ($success) { if ($success && $editor_changed) {
$_SESSION['success'] = $message; $conversion_success = $bookModel->convertChaptersContent($book_id, $old_editor_type, $editor_type);
redirect('books.php'); if (!$conversion_success) {
} else { $_SESSION['warning'] = "Книга обновлена, но возникли ошибки при конвертации содержания глав";
$_SESSION['error'] = $message; } else {
} $_SESSION['info'] = "Книга обновлена. Содержание глав сконвертировано в новый формат редактора.";
} }
} }
$page_title = $is_edit ? "Редактирование книги" : "Создание новой книги"; $message = $success ? "Книга успешно обновлена" : "Ошибка при обновлении книги";
include 'views/header.php'; } else {
?> $success = $bookModel->create($data);
$message = $success ? "Книга успешно создана" : "Ошибка при создании книги";
<form method="post" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="<?= generate_csrf_token() ?>"> if ($success) {
$new_book_id = $pdo->lastInsertId();
<div style="max-width: 100%; margin-bottom: 0.5rem;"> redirect("book_edit.php?id=$new_book_id");
<label for="title" style="display: block; margin-bottom: 0.5rem; font-weight: bold;"> }
Название книги * }
</label>
<input type="text" id="title" name="title" if ($success) {
value="<?= e($book['title'] ?? $_POST['title'] ?? '') ?>" $_SESSION['success'] = $message;
placeholder="Введите название книги" redirect('books.php');
style="width: 100%; margin-bottom: 1.5rem;" } else {
required> $_SESSION['error'] = $message;
}
<label for="genre" style="display: block; margin-bottom: 0.5rem; font-weight: bold;"> }
Жанр }
</label>
<input type="text" id="genre" name="genre" $page_title = $is_edit ? "Редактирование книги" : "Создание новой книги";
value="<?= e($book['genre'] ?? $_POST['genre'] ?? '') ?>" include 'views/header.php';
placeholder="Например: Фантастика, Роман, Детектив..." ?>
style="width: 100%; margin-bottom: 1.5rem;">
<label for="series_id" style="display: block; margin-bottom: 0.5rem; font-weight: bold;"> <form method="post" enctype="multipart/form-data">
Серия <input type="hidden" name="csrf_token" value="<?= generate_csrf_token() ?>">
</label>
<select id="series_id" name="series_id" style="width: 100%; margin-bottom: 1rem;"> <div style="max-width: 100%; margin-bottom: 0.5rem;">
<option value="">-- Без серии --</option> <label for="title" style="display: block; margin-bottom: 0.5rem; font-weight: bold;">
<?php Название книги *
$seriesModel = new Series($pdo); </label>
$user_series = $seriesModel->findByUser($user_id, false); <input type="text" id="title" name="title"
value="<?= e($book['title'] ?? $_POST['title'] ?? '') ?>"
foreach ($user_series as $ser): placeholder="Введите название книги"
$selected = ($ser['id'] == ($book['series_id'] ?? 0)) ? 'selected' : ''; style="width: 100%; margin-bottom: 1.5rem;"
?> required>
<option value="<?= $ser['id'] ?>" <?= $selected ?>>
<?= e($ser['title']) ?> <label for="genre" style="display: block; margin-bottom: 0.5rem; font-weight: bold;">
</option> Жанр
<?php endforeach; ?> </label>
</select> <input type="text" id="genre" name="genre"
value="<?= e($book['genre'] ?? $_POST['genre'] ?? '') ?>"
<label for="sort_order_in_series" style="display: block; margin-bottom: 0.5rem; font-weight: bold;"> placeholder="Например: Фантастика, Роман, Детектив..."
Порядок в серии style="width: 100%; margin-bottom: 1.5rem;">
</label>
<input type="number" id="sort_order_in_series" name="sort_order_in_series" <label for="editor_type" style="display: block; margin-bottom: 0.5rem; font-weight: bold;">
value="<?= e($book['sort_order_in_series'] ?? '') ?>" Режим редактора
placeholder="Номер по порядку в серии" </label>
min="1" <select id="editor_type" name="editor_type" style="width: 100%; margin-bottom: 1.5rem;" onchange="showEditorWarning(this)">
style="width: 100%; margin-bottom: 1.5rem;"> <option value="markdown" <?= ($book['editor_type'] ?? 'markdown') == 'markdown' ? 'selected' : '' ?>>Markdown редактор</option>
<!-- Обложка --> <option value="html" <?= ($book['editor_type'] ?? '') == 'html' ? 'selected' : '' ?>>HTML редактор (TinyMCE)</option>
<div style="margin-bottom: 1.5rem;"> </select>
<label for="cover_image" style="display: block; margin-bottom: 0.5rem; font-weight: bold;">
Обложка книги <div id="editor_warning" style="display: none; background: #fff3cd; border: 1px solid #ffeaa7; padding: 10px; border-radius: 4px; margin-bottom: 1rem;">
</label> <strong>Внимание:</strong> При смене редактора содержимое всех глав будет автоматически сконвертировано в новый формат.
</div>
<?php if (!empty($book['cover_image'])): ?>
<div style="margin-bottom: 1rem;"> <script>
<p><strong>Текущая обложка:</strong></p> function showEditorWarning(select) {
<img src="<?= COVERS_URL . e($book['cover_image']) ?>" const warning = document.getElementById('editor_warning');
alt="Обложка" const currentEditor = '<?= $book['editor_type'] ?? 'markdown' ?>';
style="max-width: 200px; height: auto; border-radius: 4px; border: 1px solid #ddd;">
<div style="margin-top: 0.5rem;"> if (select.value !== currentEditor) {
<label style="display: inline-flex; align-items: center; gap: 0.5rem;"> warning.style.display = 'block';
<input type="checkbox" name="delete_cover" value="1"> } else {
Удалить обложку warning.style.display = 'none';
</label> }
</div> }
</div>
<?php endif; ?> // Показать предупреждение при загрузке, если редактор уже отличается
document.addEventListener('DOMContentLoaded', function() {
<input type="file" id="cover_image" name="cover_image" const currentEditor = '<?= $book['editor_type'] ?? 'markdown' ?>';
accept="image/jpeg, image/png, image/gif, image/webp" const selectedEditor = document.getElementById('editor_type').value;
style="height: 2.6rem;">
<small style="color: #666;"> if (currentEditor !== selectedEditor) {
Разрешены: JPG, PNG, GIF, WebP. Максимальный размер: 5MB. document.getElementById('editor_warning').style.display = 'block';
Рекомендуемый размер: 300×450 пикселей. }
</small> });
</script>
<?php if (!empty($cover_error)): ?> <label for="series_id" style="display: block; margin-bottom: 0.5rem; font-weight: bold;">
<div style="color: #d32f2f; margin-top: 0.5rem;"> Серия
<?= e($cover_error) ?> </label>
</div> <select id="series_id" name="series_id" style="width: 100%; margin-bottom: 1rem;">
<?php endif; ?> <option value="">-- Без серии --</option>
</div> <?php
$seriesModel = new Series($pdo);
<label for="description" style="display: block; margin-bottom: 0.5rem; font-weight: bold;"> $user_series = $seriesModel->findByUser($user_id, false);
Описание книги
</label> foreach ($user_series as $ser):
<textarea id="description" name="description" $selected = ($ser['id'] == ($book['series_id'] ?? 0)) ? 'selected' : '';
placeholder="Краткое описание сюжета или аннотация..." ?>
rows="6" <option value="<?= $ser['id'] ?>" <?= $selected ?>>
style="width: 100%;"><?= e($book['description'] ?? $_POST['description'] ?? '') ?></textarea> <?= e($ser['title']) ?>
</option>
<div> <?php endforeach; ?>
<label for="published"> </select>
<input type="checkbox" id="published" name="published" value="1"
<?= !empty($book['published']) || (!empty($_POST['published']) && $_POST['published']) ? 'checked' : '' ?>> <label for="sort_order_in_series" style="display: block; margin-bottom: 0.5rem; font-weight: bold;">
Опубликовать книгу (показывать на публичной странице автора) Порядок в серии
</label> </label>
</div> <input type="number" id="sort_order_in_series" name="sort_order_in_series"
</div> value="<?= e($book['sort_order_in_series'] ?? '') ?>"
placeholder="Номер по порядку в серии"
<div style="display: flex; gap: 5px; flex-wrap: wrap; align-items: center;"> min="1"
<button type="submit" class="contrast compact-button"> style="width: 100%; margin-bottom: 1.5rem;">
<?= $is_edit ? '💾 Сохранить изменения' : '📖 Создать книгу' ?> <!-- Обложка -->
</button> <div style="margin-bottom: 1.5rem;">
</div> <label for="cover_image" style="display: block; margin-bottom: 0.5rem; font-weight: bold;">
</form> Обложка книги
<?php if ($is_edit): ?> </label>
<form method="post" action="book_delete.php" style="display: inline;" onsubmit="return confirm('Вы уверены, что хотите удалить книгу «<?= e($book['title']) ?>»? Все главы также будут удалены.');">
<input type="hidden" name="book_id" value="<?= $book['id'] ?>"> <?php if (!empty($book['cover_image'])): ?>
<input type="hidden" name="csrf_token" value="<?= generate_csrf_token() ?>"> <div style="margin-bottom: 1rem;">
<button type="submit" class="compact-button secondary" style="background: #ff4444; border-color: #ff4444; color: white;" title="Удалить книгу"> <p><strong>Текущая обложка:</strong></p>
🗑️ <img src="<?= COVERS_URL . e($book['cover_image']) ?>"
</button> alt="Обложка"
</form> style="max-width: 200px; height: auto; border-radius: 4px; border: 1px solid #ddd;">
<?php endif ?> <div style="margin-top: 0.5rem;">
<?php if ($is_edit): ?> <label style="display: inline-flex; align-items: center; gap: 0.5rem;">
<div style="margin-top: 2rem; padding: 1rem; background: #f8f9fa; border-radius: 5px;"> <input type="checkbox" name="delete_cover" value="1">
<h3>Публичная ссылка для чтения</h3> Удалить обложку
<p style="margin-bottom: 0.5rem;">Отправьте эту ссылку читателям для просмотра опубликованных глав:</p> </label>
</div>
<div style="display: flex; gap: 5px; align-items: center; flex-wrap: wrap;"> </div>
<input type="text" <?php endif; ?>
id="share-link"
value="<?= e(SITE_URL . '/view_book.php?share_token=' . $book['share_token']) ?>" <input type="file" id="cover_image" name="cover_image"
readonly accept="image/jpeg, image/png, image/gif, image/webp"
style="flex: 1; padding: 8px; border: 1px solid #ddd; border-radius: 4px; background: white;"> style="height: 2.6rem;">
<small style="color: #666;">
<button type="button" onclick="copyShareLink()" class="compact-button secondary"> Разрешены: JPG, PNG, GIF, WebP. Максимальный размер: 5MB.
📋 Копировать Рекомендуемый размер: 300×450 пикселей.
</button> </small>
<form method="post" action="book_regenerate_token.php" style="display: inline;"> <?php if (!empty($cover_error)): ?>
<input type="hidden" name="book_id" value="<?= $book_id ?>"> <div style="color: #d32f2f; margin-top: 0.5rem;">
<input type="hidden" name="csrf_token" value="<?= generate_csrf_token() ?>"> <?= e($cover_error) ?>
<button type="submit" class="compact-button secondary" onclick="return confirm('Создать новую ссылку? Старая ссылка перестанет работать.')"> </div>
🔄 Обновить <?php endif; ?>
</button> </div>
</form>
</div> <label for="description" style="display: block; margin-bottom: 0.5rem; font-weight: bold;">
Описание книги
<p style="margin-top: 0.5rem; font-size: 0.9em; color: #666;"> </label>
<strong>Примечание:</strong> В публичном просмотре отображаются только главы со статусом "Опубликована" <textarea id="description" name="description"
</p> placeholder="Краткое описание сюжета или аннотация..."
</div> rows="6"
style="width: 100%;"><?= e($book['description'] ?? $_POST['description'] ?? '') ?></textarea>
<script>
function copyShareLink() { <div>
const shareLink = document.getElementById('share-link'); <label for="published">
shareLink.select(); <input type="checkbox" id="published" name="published" value="1"
shareLink.setSelectionRange(0, 99999); <?= !empty($book['published']) || (!empty($_POST['published']) && $_POST['published']) ? 'checked' : '' ?>>
document.execCommand('copy'); Опубликовать книгу (показывать на публичной странице автора)
</label>
// Показать уведомление </div>
const button = event.target; </div>
const originalText = button.innerHTML;
button.innerHTML = '✅ Скопировано'; <div style="display: flex; gap: 5px; flex-wrap: wrap; align-items: center;">
setTimeout(() => { <button type="submit" class="contrast button">
button.innerHTML = originalText; <?= $is_edit ? '💾 Сохранить изменения' : '📖 Создать книгу' ?>
}, 2000); </button>
} </div>
</script> </form>
<?php endif; ?> <?php if ($is_edit): ?>
<form method="post" action="book_normalize_content.php" onsubmit="return confirm('Нормализовать контент всех глав книги? Это действие нельзя отменить.')">
<?php if ($is_edit): ?> <input type="hidden" name="book_id" value="<?= $book_id ?>">
<div style="margin-top: 2rem; padding: 1rem; background: #f8f9fa; border-radius: 5px;"> <input type="hidden" name="csrf_token" value="<?= generate_csrf_token() ?>">
<h3>Экспорт книги</h3> <button type="submit" class="button secondary">🔄 Нормализовать контент глав</button>
<p style="margin-bottom: 0.5rem;">Экспортируйте книгу в различные форматы:</p> <p style="margin-top: 0.5rem; font-size: 0.8em; color: #666;">
Если контент глав отображается неправильно после смены редактора, можно нормализовать его структуру.
<div style="display: flex; gap: 5px; flex-wrap: wrap;"> </p>
<a href="export_book.php?book_id=<?= $book_id ?>&format=pdf" class="adaptive-button secondary" target="_blank"> </form>
📄 PDF
</a> <?php endif; ?>
<a href="export_book.php?book_id=<?= $book_id ?>&format=docx" class="adaptive-button secondary" target="_blank"> <?php if ($is_edit): ?>
📝 DOCX <form method="post" action="book_delete.php" style="display: inline;" onsubmit="return confirm('Вы уверены, что хотите удалить книгу «<?= e($book['title']) ?>»? Все главы также будут удалены.');">
</a> <input type="hidden" name="book_id" value="<?= $book['id'] ?>">
<a href="export_book.php?book_id=<?= $book_id ?>&format=html" class="adaptive-button secondary" target="_blank"> <input type="hidden" name="csrf_token" value="<?= generate_csrf_token() ?>">
🌐 HTML <button type="submit" class="compact secondary" style="background: #ff4444; border-color: #ff4444; color: white;" title="Удалить книгу">
</a> 🗑️ Удалить главу
<a href="export_book.php?book_id=<?= $book_id ?>&format=txt" class="adaptive-button secondary" target="_blank"> </button>
📄 TXT </form>
</a> <?php endif ?>
</div>
<?php if ($is_edit): ?>
<p style="margin-top: 0.5rem; font-size: 0.9em; color: #666;"> <div style="margin-top: 2rem; padding: 1rem; background: #f8f9fa; border-radius: 5px;">
<strong>Примечание:</strong> Экспортируются все главы книги (включая черновики) <h3>Публичная ссылка для чтения</h3>
</p> <div style="display: flex; gap: 5px; align-items: center; flex-wrap: wrap;">
</div> <input type="text"
<?php endif; ?> id="share-link"
value="<?= e(SITE_URL . '/view_book.php?share_token=' . $book['share_token']) ?>"
<?php if ($is_edit): ?> readonly
<div style="margin-top: 3rem;"> style="flex: 1; padding: 8px; border: 1px solid #ddd; border-radius: 4px; background: white; width:80%;">
<h2>Главы этой книги</h2> <br>
<a href="chapters.php?book_id=<?= $book_id ?>" class="compact-button secondary"> <button type="button" onclick="copyShareLink()" class="compact-button secondary" style="width: 15%;">
📑 Все главы 📋 Копировать
</a> </button>
&nbsp;
<a href="chapter_edit.php?book_id=<?= $book_id ?>" role="button" class="compact-button secondary"> <form method="post" action="book_regenerate_token.php" style="display: inline; margin-top: 1.5em;">
✏️ Добавить главу <input type="hidden" name="book_id" value="<?= $book_id ?>">
</a> <input type="hidden" name="csrf_token" value="<?= generate_csrf_token() ?>">
<?php <button type="submit" class="compact-button secondary" onclick="return confirm('Создать новую ссылку? Старая ссылка перестанет работать.')" >
// Получаем главы книги 🔄 Обновить
$stmt = $pdo->prepare("SELECT * FROM chapters WHERE book_id = ? ORDER BY sort_order, created_at"); </button>
$stmt->execute([$book_id]);
$chapters = $stmt->fetchAll(); </form>
<p style="margin-top: -1rem; font-size: 0.8em; color: #666; width: 100%;">
if ($chapters): ?> <strong>Примечание:</strong> В публичном просмотре отображаются только главы со статусом "Опубликована"
<div style="overflow-x: auto;"> </p>
<table style="width: 100%;"> </div>
<thead>
<tr>
<th style="text-align: left; padding: 12px 8px;">Название</th> </div>
<th style="text-align: left; padding: 12px 8px;">Статус</th>
<th style="text-align: left; padding: 12px 8px;">Слов</th> <script>
<th style="text-align: left; padding: 12px 8px;">Действия</th> function copyShareLink() {
</tr> const shareLink = document.getElementById('share-link');
</thead> shareLink.select();
<tbody> shareLink.setSelectionRange(0, 99999);
<?php foreach ($chapters as $chapter): ?> document.execCommand('copy');
<tr style="border-bottom: 1px solid #eee;">
<td style="padding: 12px 8px;"><?= e($chapter['title']) ?></td> // Показать уведомление
<td style="padding: 12px 8px;"> const button = event.target;
<span style="color: <?= $chapter['status'] == 'published' ? 'green' : 'orange' ?>"> const originalText = button.innerHTML;
<?= $chapter['status'] == 'published' ? 'Опубликована' : 'Черновик' ?> button.innerHTML = '✅ Скопировано';
</span> setTimeout(() => {
</td> button.innerHTML = originalText;
<td style="padding: 12px 8px;"><?= $chapter['word_count'] ?></td> }, 2000);
<td style="padding: 12px 8px;"> }
<a href="chapter_edit.php?id=<?= $chapter['id'] ?>" role="button" class="compact-button secondary" style="text-decoration: none;"> </script>
Редактировать <?php endif; ?>
</a>
</td> <?php if ($is_edit): ?>
</tr> <div style="margin-top: 2rem; padding: 1rem; background: #f8f9fa; border-radius: 5px;">
<?php endforeach; ?> <h3>Экспорт книги</h3>
</tbody> <p style="margin-bottom: 0.5rem;">Экспортируйте книгу в различные форматы:</p>
</table>
</div> <div style="display: flex; gap: 5px; flex-wrap: wrap;">
<?php else: ?> <a href="export_book.php?book_id=<?= $book_id ?>&format=pdf" class="adaptive-button secondary" target="_blank">
<div style="text-align: center; padding: 2rem; background: #f9f9f9; border-radius: 5px;"> 📄 PDF
<p style="margin-bottom: 1rem;">В этой книге пока нет глав.</p> </a>
<a href="chapter_edit.php?book_id=<?= $book_id ?>" role="button" class="compact-button secondary" > <a href="export_book.php?book_id=<?= $book_id ?>&format=docx" class="adaptive-button secondary" target="_blank">
✏️ Добавить первую главу 📝 DOCX
</a> </a>
</div> <a href="export_book.php?book_id=<?= $book_id ?>&format=html" class="adaptive-button secondary" target="_blank">
<?php endif; ?> 🌐 HTML
</div> </a>
<?php endif; ?> <a href="export_book.php?book_id=<?= $book_id ?>&format=txt" class="adaptive-button secondary" target="_blank">
📄 TXT
</a>
</div>
<p style="margin-top: 0.5rem; font-size: 0.9em; color: #666;">
<strong>Примечание:</strong> Экспортируются все главы книги (включая черновики)
</p>
</div>
<?php endif; ?>
<?php if ($is_edit): ?>
<div style="margin-top: 3rem;">
<h2>Главы этой книги</h2>
<a href="chapters.php?book_id=<?= $book_id ?>" class="compact-button secondary">
📑 Все главы
</a>
&nbsp;
<a href="chapter_edit.php?book_id=<?= $book_id ?>" role="button" class="compact-button secondary">
✏️ Добавить главу
</a>
<?php
// Получаем главы книги
$stmt = $pdo->prepare("SELECT * FROM chapters WHERE book_id = ? ORDER BY sort_order, created_at");
$stmt->execute([$book_id]);
$chapters = $stmt->fetchAll();
if ($chapters): ?>
<div style="overflow-x: auto;">
<table style="width: 100%;">
<thead>
<tr>
<th style="text-align: left; padding: 12px 8px;">Название</th>
<th style="text-align: left; padding: 12px 8px;">Статус</th>
<th style="text-align: left; padding: 12px 8px;">Слов</th>
<th style="text-align: left; padding: 12px 8px;">Действия</th>
</tr>
</thead>
<tbody>
<?php foreach ($chapters as $chapter): ?>
<tr style="border-bottom: 1px solid #eee;">
<td style="padding: 12px 8px;"><?= e($chapter['title']) ?></td>
<td style="padding: 12px 8px;">
<span style="color: <?= $chapter['status'] == 'published' ? 'green' : 'orange' ?>">
<?= $chapter['status'] == 'published' ? 'Опубликована' : 'Черновик' ?>
</span>
</td>
<td style="padding: 12px 8px;"><?= $chapter['word_count'] ?></td>
<td style="padding: 12px 8px;">
<a href="chapter_edit.php?id=<?= $chapter['id'] ?>" role="button" class="compact-button secondary" style="text-decoration: none;">
Редактировать
</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div style="text-align: center; padding: 2rem; background: #f9f9f9; border-radius: 5px;">
<p style="margin-bottom: 1rem;">В этой книге пока нет глав.</p>
<a href="chapter_edit.php?book_id=<?= $book_id ?>" role="button" class="compact-button secondary" >
✏️ Добавить первую главу
</a>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
<?php include 'views/footer.php'; ?> <?php include 'views/footer.php'; ?>

310
books.php
View File

@ -1,156 +1,156 @@
<?php <?php
require_once 'config/config.php'; require_once 'config/config.php';
require_login(); require_login();
$user_id = $_SESSION['user_id']; $user_id = $_SESSION['user_id'];
$bookModel = new Book($pdo); $bookModel = new Book($pdo);
$books = $bookModel->findByUser($user_id); $books = $bookModel->findByUser($user_id);
$page_title = "Мои книги"; $page_title = "Мои книги";
include 'views/header.php'; include 'views/header.php';
?> ?>
<h1>Мои книги</h1> <h1>Мои книги</h1>
<?php if (isset($_SESSION['success'])): ?> <?php if (isset($_SESSION['success'])): ?>
<div class="alert alert-success"> <div class="alert alert-success">
<?= e($_SESSION['success']) ?> <?= e($_SESSION['success']) ?>
<?php unset($_SESSION['success']); ?> <?php unset($_SESSION['success']); ?>
</div> </div>
<?php endif; ?> <?php endif; ?>
<?php if (isset($_SESSION['error'])): ?> <?php if (isset($_SESSION['error'])): ?>
<div class="alert alert-error"> <div class="alert alert-error">
<?= e($_SESSION['error']) ?> <?= e($_SESSION['error']) ?>
<?php unset($_SESSION['error']); ?> <?php unset($_SESSION['error']); ?>
</div> </div>
<?php endif; ?> <?php endif; ?>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<h2 style="margin: 0;">Всего книг: <?= count($books) ?></h2> <h2 style="margin: 0;">Всего книг: <?= count($books) ?></h2>
<div style="display: flex; gap: 10px; align-items: center;"> <div style="display: flex; gap: 10px; align-items: center;">
<a href="book_edit.php" class="action-button primary"> Новая книга</a> <a href="book_edit.php" class="action-button primary"> Новая книга</a>
<?php if (!empty($books)): ?> <?php if (!empty($books)): ?>
<button type="button" onclick="showDeleteConfirmation()" class="action-button delete"> <button type="button" onclick="showDeleteConfirmation()" class="action-button delete">
🗑️ Удалить все книги 🗑️ Удалить все книги
</button> </button>
<?php endif; ?> <?php endif; ?>
</div> </div>
</div> </div>
<?php if (empty($books)): ?> <?php if (empty($books)): ?>
<article style="text-align: center; padding: 2rem;"> <article style="text-align: center; padding: 2rem;">
<h3>У вас пока нет книг</h3> <h3>У вас пока нет книг</h3>
<p>Создайте свою первую книгу и начните писать!</p> <p>Создайте свою первую книгу и начните писать!</p>
<a href="book_edit.php" role="button">📖 Создать первую книгу</a> <a href="book_edit.php" role="button">📖 Создать первую книгу</a>
</article> </article>
<?php else: ?> <?php else: ?>
<div class="flex"> <div class="flex">
<?php foreach ($books as $book): ?> <?php foreach ($books as $book): ?>
<article> <article>
<?php if ($book['cover_image']): ?> <?php if ($book['cover_image']): ?>
<div style="text-align: center; margin-bottom: 1rem; float: left; margin-right: 2em;"> <div style="text-align: center; margin-bottom: 1rem; float: left; margin-right: 2em;">
<img src="<?= COVERS_URL . e($book['cover_image']) ?>" <img src="<?= COVERS_URL . e($book['cover_image']) ?>"
alt="<?= e($book['title']) ?>" alt="<?= e($book['title']) ?>"
style="max-width: 120px; height: auto; border-radius: 4px; border: 1px solid #ddd;"> style="max-width: 120px; height: auto; border-radius: 4px; border: 1px solid #ddd;">
</div> </div>
<?php endif; ?> <?php endif; ?>
<header> <header>
<h3><?= e($book['title']) ?> <h3><?= e($book['title']) ?>
<?php if ($book['series_id']): ?> <?php if ($book['series_id']): ?>
<?php <?php
$series_stmt = $pdo->prepare("SELECT title FROM series WHERE id = ?"); $series_stmt = $pdo->prepare("SELECT title FROM series WHERE id = ?");
$series_stmt->execute([$book['series_id']]); $series_stmt->execute([$book['series_id']]);
$series_title = $series_stmt->fetch()['title'] ?? ''; $series_title = $series_stmt->fetch()['title'] ?? '';
?> ?>
<?php if ($series_title): ?> <?php if ($series_title): ?>
<div style="margin: 0.3rem 0;"> <div style="margin: 0.3rem 0;">
<small style="color: #007bff;"> <small style="color: #007bff;">
📚 Серия: <?= e($series_title) ?> 📚 Серия: <?= e($series_title) ?>
<?php if ($book['sort_order_in_series']): ?> <?php if ($book['sort_order_in_series']): ?>
(Книга <?= $book['sort_order_in_series'] ?>) (Книга <?= $book['sort_order_in_series'] ?>)
<?php endif; ?> <?php endif; ?>
</small> </small>
</div> </div>
<?php endif; ?> <?php endif; ?>
<?php endif; ?> <?php endif; ?>
<div style="display: flex; gap: 3px; float:right;"> <div style="display: flex; gap: 3px; float:right;">
<a href="book_edit.php?id=<?= $book['id'] ?>" class="compact-button secondary" title="Редактировать книгу"> <a href="book_edit.php?id=<?= $book['id'] ?>" class="compact-button secondary" title="Редактировать книгу">
✏️ ✏️
</a> </a>
<a href="view_book.php?share_token=<?= $book['share_token'] ?>" class="compact-button secondary" title="Просмотреть книгу" target="_blank"> <a href="view_book.php?share_token=<?= $book['share_token'] ?>" class="compact-button secondary" title="Просмотреть книгу" target="_blank">
👁️ 👁️
</a> </a>
<a href="chapters.php?book_id=<?= $book['id'] ?>" class="compact-button secondary" title="Просмотр глав"> <a href="chapters.php?book_id=<?= $book['id'] ?>" class="compact-button secondary" title="Просмотр глав">
📑 📑
</a> </a>
<a href="export_book.php?book_id=<?= $book['id'] ?>&format=pdf" class="compact-button secondary" title="Экспорт в PDF" target="_blank"> <a href="export_book.php?book_id=<?= $book['id'] ?>&format=pdf" class="compact-button secondary" title="Экспорт в PDF" target="_blank">
📄 📄
</a> </a>
<form method="post" action="book_delete.php" style="display: inline; margin-top: -0.1em;" onsubmit="return confirm('Вы уверены, что хотите удалить книгу «<?= e($book['title']) ?>»? Все главы также будут удалены.');"> <form method="post" action="book_delete.php" style="display: inline; margin-top: -0.1em;" onsubmit="return confirm('Вы уверены, что хотите удалить книгу «<?= e($book['title']) ?>»? Все главы также будут удалены.');">
<input type="hidden" name="book_id" value="<?= $book['id'] ?>"> <input type="hidden" name="book_id" value="<?= $book['id'] ?>">
<input type="hidden" name="csrf_token" value="<?= generate_csrf_token() ?>"> <input type="hidden" name="csrf_token" value="<?= generate_csrf_token() ?>">
<button type="submit" class="compact-button secondary" style="background: #ff4444; border-color: #ff4444; color: white;" title="Удалить книгу"> <button type="submit" class="compact-button secondary" style="background: #ff4444; border-color: #ff4444; color: white;" title="Удалить книгу">
🗑️ 🗑️
</button> </button>
</form> </form>
</div> </div>
</h3> </h3>
<?php if ($book['genre']): ?> <?php if ($book['genre']): ?>
<small style="color: #666; margin-top: 0.1em;"><?= e($book['genre']) ?></small> <small style="color: #666; margin-top: 0.1em;"><?= e($book['genre']) ?></small>
<?php endif; ?> <?php endif; ?>
</header> </header>
<?php if ($book['description']): ?> <?php if ($book['description']): ?>
<p><?= e(mb_strimwidth($book['description'], 0, 150, '...')) ?></p> <p><?= e(mb_strimwidth($book['description'], 0, 150, '...')) ?></p>
<?php endif; ?> <?php endif; ?>
<footer style="margin-top:1em; padding-top:2em;"> <footer style="margin-top:1em; padding-top:2em;">
<div> <div>
<small> <small>
Глав: <?= $book['chapter_count'] ?> | Глав: <?= $book['chapter_count'] ?> |
Слов: <?= $book['total_words'] ?> Слов: <?= $book['total_words'] ?>
</small> </small>
</div> </div>
</footer> </footer>
</article> </article>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>
<!-- Подтверждение удаления всех книг --> <!-- Подтверждение удаления всех книг -->
<dialog id="deleteAllDialog" style="border-radius: 8px; padding: 20px; max-width: 500px; background-color: #fff;"> <dialog id="deleteAllDialog" style="border-radius: 8px; padding: 20px; max-width: 500px; background-color: #fff;">
<h3 style="margin-top: 0;">Удалить все книги?</h3> <h3 style="margin-top: 0;">Удалить все книги?</h3>
<p>Это действие удалит все ваши книги и все связанные с ними главы. Это действие нельзя отменить.</p> <p>Это действие удалит все ваши книги и все связанные с ними главы. Это действие нельзя отменить.</p>
<form method="post" action="book_delete_all.php" style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px;"> <form method="post" action="book_delete_all.php" style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px;">
<input type="hidden" name="csrf_token" value="<?= generate_csrf_token() ?>"> <input type="hidden" name="csrf_token" value="<?= generate_csrf_token() ?>">
<button type="button" onclick="closeDeleteDialog()" class="secondary" style="flex: 1;"> <button type="button" onclick="closeDeleteDialog()" class="secondary" style="flex: 1;">
Отмена Отмена
</button> </button>
<button type="submit" class="contrast" style="flex: 1; background: #ff4444; border-color: #ff4444; color: white;"> <button type="submit" class="contrast" style="flex: 1; background: #ff4444; border-color: #ff4444; color: white;">
🗑️ Удалить все 🗑️ Удалить все
</button> </button>
</form> </form>
</dialog> </dialog>
<script> <script>
function showDeleteConfirmation() { function showDeleteConfirmation() {
const dialog = document.getElementById('deleteAllDialog'); const dialog = document.getElementById('deleteAllDialog');
dialog.showModal(); dialog.showModal();
} }
function closeDeleteDialog() { function closeDeleteDialog() {
const dialog = document.getElementById('deleteAllDialog'); const dialog = document.getElementById('deleteAllDialog');
dialog.close(); dialog.close();
} }
// Закрытие диалога по клику вне его области // Закрытие диалога по клику вне его области
document.getElementById('deleteAllDialog').addEventListener('click', function(event) { document.getElementById('deleteAllDialog').addEventListener('click', function(event) {
if (event.target === this) { if (event.target === this) {
closeDeleteDialog(); closeDeleteDialog();
} }
}); });
</script> </script>
<?php endif; ?> <?php endif; ?>
<?php include 'views/footer.php'; ?> <?php include 'views/footer.php'; ?>

View File

@ -1,43 +1,43 @@
<?php <?php
require_once 'config/config.php'; require_once 'config/config.php';
require_login(); require_login();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
$_SESSION['error'] = "Неверный метод запроса"; $_SESSION['error'] = "Неверный метод запроса";
redirect('books.php'); redirect('books.php');
} }
if (!verify_csrf_token($_POST['csrf_token'] ?? '')) { if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
$_SESSION['error'] = "Ошибка безопасности"; $_SESSION['error'] = "Ошибка безопасности";
redirect('books.php'); redirect('books.php');
} }
$chapter_id = $_POST['chapter_id'] ?? null; $chapter_id = $_POST['chapter_id'] ?? null;
$user_id = $_SESSION['user_id']; $user_id = $_SESSION['user_id'];
if (!$chapter_id) { if (!$chapter_id) {
$_SESSION['error'] = "Не указана глава для удаления"; $_SESSION['error'] = "Не указана глава для удаления";
redirect('books.php'); redirect('books.php');
} }
$chapterModel = new Chapter($pdo); $chapterModel = new Chapter($pdo);
// Проверяем права доступа // Проверяем права доступа
if (!$chapterModel->userOwnsChapter($chapter_id, $user_id)) { if (!$chapterModel->userOwnsChapter($chapter_id, $user_id)) {
$_SESSION['error'] = "У вас нет доступа к этой главе"; $_SESSION['error'] = "У вас нет доступа к этой главе";
redirect('books.php'); redirect('books.php');
} }
$chapter = $chapterModel->findById($chapter_id); $chapter = $chapterModel->findById($chapter_id);
$book_id = $chapter['book_id']; $book_id = $chapter['book_id'];
// Удаляем главу // Удаляем главу
if ($chapterModel->delete($chapter_id)) { if ($chapterModel->delete($chapter_id)) {
$_SESSION['success'] = "Глава успешно удалена"; $_SESSION['success'] = "Глава успешно удалена";
} else { } else {
$_SESSION['error'] = "Ошибка при удалении главы"; $_SESSION['error'] = "Ошибка при удалении главы";
} }
redirect("chapters.php?book_id=$book_id"); redirect("chapters.php?book_id=$book_id");
?> ?>

View File

@ -1,291 +1,386 @@
<?php <?php
require_once 'config/config.php'; require_once 'config/config.php';
require_login(); require_login();
$user_id = $_SESSION['user_id']; $user_id = $_SESSION['user_id'];
$chapterModel = new Chapter($pdo); $chapterModel = new Chapter($pdo);
$bookModel = new Book($pdo); $bookModel = new Book($pdo);
// Получаем book_id из GET или из существующей главы // Получаем book_id из GET или из существующей главы
$chapter_id = $_GET['id'] ?? null; $chapter_id = $_GET['id'] ?? null;
$book_id = $_GET['book_id'] ?? null; $book_id = $_GET['book_id'] ?? null;
$chapter = null; $chapter = null;
$is_edit = false; $is_edit = false;
// Если редактируем существующую главу // Если редактируем существующую главу
if ($chapter_id) { if ($chapter_id) {
$chapter = $chapterModel->findById($chapter_id); $chapter = $chapterModel->findById($chapter_id);
if (!$chapter || $chapter['user_id'] != $user_id) { if (!$chapter || $chapter['user_id'] != $user_id) {
$_SESSION['error'] = "Глава не найдена или у вас нет доступа"; $_SESSION['error'] = "Глава не найдена или у вас нет доступа";
redirect('books.php'); redirect('books.php');
} }
$book_id = $chapter['book_id']; $book_id = $chapter['book_id'];
$is_edit = true; $is_edit = true;
} }
if (!$book_id) { if (!$book_id) {
$_SESSION['error'] = "Не указана книга"; $_SESSION['error'] = "Не указана книга";
redirect('books.php'); redirect('books.php');
} }
if (!$bookModel->userOwnsBook($book_id, $user_id)) { if (!$bookModel->userOwnsBook($book_id, $user_id)) {
$_SESSION['error'] = "У вас нет доступа к этой книге"; $_SESSION['error'] = "У вас нет доступа к этой книге";
redirect('books.php'); redirect('books.php');
} }
// Получаем информацию о книге // Получаем информацию о книге
$book = $bookModel->findById($book_id); $book = $bookModel->findById($book_id);
// Обработка формы // Обработка формы
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!verify_csrf_token($_POST['csrf_token'] ?? '')) { if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
$_SESSION['error'] = "Ошибка безопасности"; $_SESSION['error'] = "Ошибка безопасности";
redirect($is_edit ? "chapter_edit.php?id=$chapter_id" : "chapter_edit.php?book_id=$book_id"); redirect($is_edit ? "chapter_edit.php?id=$chapter_id" : "chapter_edit.php?book_id=$book_id");
} }
// Обработка автосохранения // Обработка автосохранения
if (isset($_POST['autosave']) && $_POST['autosave'] === 'true') { if (isset($_POST['autosave']) && $_POST['autosave'] === 'true') {
// Автосохранение работает только для существующих глав // Автосохранение работает только для существующих глав
// Если это не редактирование, игнорируем автосохранение // Если это не редактирование, игнорируем автосохранение
if (!$is_edit) { if (!$is_edit) {
header('Content-Type: application/json'); header('Content-Type: application/json');
echo json_encode(['success' => false, 'message' => 'Автосохранение недоступно для новых глав']); echo json_encode(['success' => false, 'message' => 'Автосохранение недоступно для новых глав']);
exit; exit;
} }
$title = trim($_POST['title'] ?? ''); $title = trim($_POST['title'] ?? '');
$content = trim($_POST['content'] ?? ''); $content = trim($_POST['content'] ?? '');
$status = $_POST['status'] ?? 'draft'; $status = $_POST['status'] ?? 'draft';
if (empty($title)) { if (empty($title)) {
header('Content-Type: application/json'); header('Content-Type: application/json');
echo json_encode(['success' => false, 'message' => 'Название главы обязательно']); echo json_encode(['success' => false, 'message' => 'Название главы обязательно']);
exit; exit;
} }
$data = [ $data = [
'title' => $title, 'title' => $title,
'content' => $content, 'content' => $content,
'status' => $status, 'status' => $status,
'book_id' => $book_id 'book_id' => $book_id
]; ];
$success = $chapterModel->update($chapter_id, $data); $success = $chapterModel->update($chapter_id, $data);
header('Content-Type: application/json'); header('Content-Type: application/json');
echo json_encode(['success' => $success]); echo json_encode(['success' => $success]);
exit; exit;
} }
// Обычная обработка формы (не автосохранение) // Обычная обработка формы (не автосохранение)
$title = trim($_POST['title'] ?? ''); $title = trim($_POST['title'] ?? '');
$content = trim($_POST['content'] ?? ''); $content = trim($_POST['content'] ?? '');
$status = $_POST['status'] ?? 'draft'; $status = $_POST['status'] ?? 'draft';
if (empty($title)) { if (empty($title)) {
$_SESSION['error'] = "Название главы обязательно"; $_SESSION['error'] = "Название главы обязательно";
} else { } else {
$data = [ $data = [
'title' => $title, 'title' => $title,
'content' => $content, 'content' => $content,
'status' => $status, 'status' => $status,
'book_id' => $book_id 'book_id' => $book_id
]; ];
if ($is_edit) { if ($is_edit) {
$success = $chapterModel->update($chapter_id, $data); $success = $chapterModel->update($chapter_id, $data);
$message = $success ? "Глава успешно обновлена" : "Ошибка при обновлении главы"; $message = $success ? "Глава успешно обновлена" : "Ошибка при обновлении главы";
} else { } else {
$success = $chapterModel->create($data); $success = $chapterModel->create($data);
$message = $success ? "Глава успешно создана" : "Ошибка при создании главы"; $message = $success ? "Глава успешно создана" : "Ошибка при создании главы";
if ($success) { if ($success) {
$new_chapter_id = $pdo->lastInsertId(); $new_chapter_id = $pdo->lastInsertId();
redirect("chapter_edit.php?id=$new_chapter_id"); redirect("chapter_edit.php?id=$new_chapter_id");
} }
} }
if ($success) { if ($success) {
$_SESSION['success'] = $message; $_SESSION['success'] = $message;
redirect("book_edit.php?id=$book_id"); redirect("book_edit.php?id=$book_id");
} else { } else {
$_SESSION['error'] = $message; $_SESSION['error'] = $message;
} }
} }
} }
$page_title = $is_edit ? "Редактирование главы" : "Создание новой главы"; $page_title = $is_edit ? "Редактирование главы" : "Создание новой главы";
include 'views/header.php'; include 'views/header.php';
?> ?>
<?php if ($is_edit): ?> <?php if ($is_edit): ?>
<div style="margin-top: 1rem;"> <div style="margin-top: 1rem;">
<div style="display: flex; gap: 10px; flex-wrap: wrap;"> <div style="display: flex; gap: 10px; flex-wrap: wrap;">
<?php <?php
// Получаем все главы книги для навигации // Получаем все главы книги для навигации
$chapters = $chapterModel->findByBook($book_id); $chapters = $chapterModel->findByBook($book_id);
$current_index = null; $current_index = null;
// Находим индекс текущей главы // Находим индекс текущей главы
foreach ($chapters as $index => $chap) { foreach ($chapters as $index => $chap) {
if ($chap['id'] == $chapter_id) { if ($chap['id'] == $chapter_id) {
$current_index = $index; $current_index = $index;
break; break;
} }
} }
if ($current_index !== null && $current_index > 0): if ($current_index !== null && $current_index > 0):
$prev_chapter = $chapters[$current_index - 1]; $prev_chapter = $chapters[$current_index - 1];
?> ?>
<a href="chapter_edit.php?id=<?= $prev_chapter['id'] ?>" role="button" class="secondary" style="padding: 2px 4px;"> <a href="chapter_edit.php?id=<?= $prev_chapter['id'] ?>" role="button" class="secondary" style="padding: 2px 4px;">
⬅️ Предыдущая: <?= e(mb_strimwidth($prev_chapter['title'], 0, 30, '...')) ?> ⬅️ Предыдущая: <?= e(mb_strimwidth($prev_chapter['title'], 0, 30, '...')) ?>
</a> </a>
<?php endif; ?> <?php endif; ?>
<?php if ($current_index !== null && $current_index < count($chapters) - 1): <?php if ($current_index !== null && $current_index < count($chapters) - 1):
$next_chapter = $chapters[$current_index + 1]; $next_chapter = $chapters[$current_index + 1];
?> ?>
<a href="chapter_edit.php?id=<?= $next_chapter['id'] ?>" role="button" class="secondary" style="padding: 2px 4px;"> <a href="chapter_edit.php?id=<?= $next_chapter['id'] ?>" role="button" class="secondary" style="padding: 2px 4px;">
Следующая: <?= e(mb_strimwidth($next_chapter['title'], 0, 30, '...')) ?> ➡️ Следующая: <?= e(mb_strimwidth($next_chapter['title'], 0, 30, '...')) ?> ➡️
</a> </a>
<?php endif; ?> <?php endif; ?>
</div> </div>
</div> </div>
<?php endif; ?> <?php endif; ?>
<h1><?= $is_edit ? "Редактирование главы" : "Создание новой главы" ?></h1> <h1><?= $is_edit ? "Редактирование главы" : "Создание новой главы" ?></h1>
<p><strong>Книга:</strong> <?= e($book['title']) ?></p> <p><strong>Книга:</strong> <?= e($book['title']) ?></p>
<?php if (isset($_SESSION['error'])): ?> <?php if (isset($_SESSION['error'])): ?>
<div class="alert alert-error"> <div class="alert alert-error">
<?= e($_SESSION['error']) ?> <?= e($_SESSION['error']) ?>
<?php unset($_SESSION['error']); ?> <?php unset($_SESSION['error']); ?>
</div> </div>
<?php endif; ?> <?php endif; ?>
<form method="post" id="main-form"> <form method="post" id="main-form">
<input type="hidden" name="csrf_token" value="<?= generate_csrf_token() ?>"> <input type="hidden" name="csrf_token" value="<?= generate_csrf_token() ?>">
<div style="max-width: 100%; margin-bottom: 1rem;"> <div style="max-width: 100%; margin-bottom: 1rem;">
<label for="title" style="display: block; margin-bottom: 0.5rem; font-weight: bold;"> <label for="title" style="display: block; margin-bottom: 0.5rem; font-weight: bold;">
Название главы * Название главы *
</label> </label>
<input type="text" id="title" name="title" <input type="text" id="title" name="title"
value="<?= e($chapter['title'] ?? $_POST['title'] ?? '') ?>" value="<?= e($chapter['title'] ?? $_POST['title'] ?? '') ?>"
placeholder="Введите название главы" placeholder="Введите название главы"
style="width: 100%; margin-bottom: 1.5rem;" style="width: 100%; margin-bottom: 1.5rem;"
required> required>
<label for="status" style="display: block; margin-bottom: 0.5rem; font-weight: bold;"> <label for="status" style="display: block; margin-bottom: 0.5rem; font-weight: bold;">
Статус Статус
</label> </label>
<select id="status" name="status" style="width: 100%; margin-bottom: 1.5rem;"> <select id="status" name="status" style="width: 100%; margin-bottom: 1.5rem;">
<option value="draft" <?= ($chapter['status'] ?? 'draft') == 'draft' ? 'selected' : '' ?>>Черновик</option> <option value="draft" <?= ($chapter['status'] ?? 'draft') == 'draft' ? 'selected' : '' ?>>Черновик</option>
<option value="published" <?= ($chapter['status'] ?? '') == 'published' ? 'selected' : '' ?>>Опубликована</option> <option value="published" <?= ($chapter['status'] ?? '') == 'published' ? 'selected' : '' ?>>Опубликована</option>
</select> </select>
<label for="content" style="display: block; margin-bottom: 0; font-weight: bold;"> <label for="content" style="display: block; margin-bottom: 0; font-weight: bold;">
Содержание главы Содержание главы
</label> <?php if (isset($book['editor_type'])): ?>
<textarea name="content" id="content" <small style="color: #666; font-weight: normal;">
placeholder="Начните писать вашу главу здесь..." (Режим: <?= $book['editor_type'] == 'markdown' ? 'Markdown' : 'HTML' ?>)
rows="15" </small>
style="width: 100%; font-family: monospace;"><?= e($chapter['content'] ?? $_POST['content'] ?? '') ?></textarea> <?php endif; ?>
<?php if ($is_edit && isset($chapter['word_count'])): ?> </label>
<div style="background: #f5f5f5; padding: 10px; border-radius: 5px; margin-bottom: 1rem;">
<strong>Статистика:</strong> <?= $chapter['word_count'] ?> слов <?php if (($book['editor_type'] ?? 'markdown') === 'html'): ?>
| Обновлено: <?= date('d.m.Y H:i', strtotime($chapter['updated_at'])) ?> <!-- HTML редактор (TinyMCE) -->
</div> <textarea name="content" id="content" style="width: 100%; min-height: 500px;">
<?php endif; ?> <?= e($chapter['content'] ?? $_POST['content'] ?? '') ?>
</div> </textarea>
</form>
<!-- Подключаем TinyMCE -->
<div class="button-group"> <script src="https://cdnjs.cloudflare.com/ajax/libs/tinymce/6.8.6/tinymce.min.js" referrerpolicy="origin"></script>
<button type="submit" form="main-form" class="contrast"> <script>
<?= $is_edit ? '💾 Сохранить изменения' : '📝 Создать главу' ?> tinymce.init({
</button> selector: '#content',
plugins: 'advlist autolink lists link image charmap preview anchor pagebreak searchreplace wordcount visualblocks visualchars code fullscreen insertdatetime media table emoticons',
<a href="book_edit.php?id=<?= $book_id ?>" role="button" class="secondary"> toolbar: 'undo redo | blocks | bold italic underline strikethrough | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image media | code preview fullscreen',
Отмена menubar: 'edit view insert format tools table',
</a> height: 500,
language: 'ru',
<button type="button" class="green-btn" id="preview-button"> branding: false,
👁️ Предпросмотр promotion: false,
</button> image_advtab: true,
</div>
// Важные настройки для сохранения структуры
<!-- Форма для предпросмотра --> forced_root_block: 'p', // Используем <p> вместо <div>
<form method="post" action="preview.php" target="_blank" id="preview-form" style="display: none;"> force_br_newlines: false, // Не использовать <br> вместо абзацев
<input type="hidden" name="content" id="preview-content"> force_p_newlines: true, // Всегда создавать новые абзацы при Enter
<input type="hidden" name="title" id="preview-title" value="<?= e($chapter['title'] ?? 'Новая глава') ?>"> convert_newlines_to_brs: false, // Не конвертировать переносы в <br>
</form> remove_trailing_brs: true, // Убирать лишние <br> в конце
<?php if ($is_edit): ?> // Настройки форматирования
<div class="button-group"> formats: {
<a href="chapter_edit.php?book_id=<?= $book_id ?>" role="button"> // Сохраняем семантическое форматирование
Новая глава bold: { inline: 'strong' },
</a> italic: { inline: 'em' },
underline: { inline: 'u', exact: true },
<form method="post" action="chapter_delete.php" style="flex: 1;" onsubmit="return confirm('Вы уверены, что хотите удалить эту главу? Это действие нельзя отменить.');"> strikethrough: { inline: 'del' }
<input type="hidden" name="chapter_id" value="<?= $chapter_id ?>"> },
<input type="hidden" name="csrf_token" value="<?= generate_csrf_token() ?>">
<button type="submit" class="secondary delete-btn"> // Настройки контента
🗑️ Удалить content_style: `
</button> body {
</form> font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
</div> font-size: 14px;
<?php endif; ?> line-height: 1.6;
margin: 0;
<?php if ($is_edit): ?> padding: 10px;
<div style="margin-top: 3rem;"> }
<div style="display: flex; gap: 10px; flex-wrap: wrap;"> p {
<?php margin: 0 0 1em 0;
// Получаем все главы книги для навигации }
$chapters = $chapterModel->findByBook($book_id); h1, h2, h3, h4, h5, h6 {
$current_index = null; margin: 1em 0 0.5em 0;
}
// Находим индекс текущей главы `,
foreach ($chapters as $index => $chap) {
if ($chap['id'] == $chapter_id) { // Настройки для чистого HTML
$current_index = $index; valid_elements: '*[*]', // Разрешаем все элементы (можно ограничить при необходимости)
break; valid_children: '+body[p,div,h1,h2,h3,h4,h5,h6,blockquote,pre,ul,ol,li,table]',
}
} // Автосохранение
setup: function (editor) {
if ($current_index !== null && $current_index > 0): editor.on('init', function () {
$prev_chapter = $chapters[$current_index - 1]; // Нормализуем контент при инициализации
?> var content = editor.getContent();
<a href="chapter_edit.php?id=<?= $prev_chapter['id'] ?>" role="button" class="secondary" style="padding: 2px 4px;"> if (content && !content.match(/<p[^>]*>/) && content.trim().length > 0) {
⬅️ Предыдущая: <?= e(mb_strimwidth($prev_chapter['title'], 0, 30, '...')) ?> // Если нет тегов абзацев, оборачиваем в <p>
</a> editor.setContent('<p>' + content.replace(/\n/g, '</p><p>') + '</p>');
<?php endif; ?> }
});
<?php if ($current_index !== null && $current_index < count($chapters) - 1):
$next_chapter = $chapters[$current_index + 1]; editor.on('keydown', function (e) {
?> clearTimeout(window.tinymceSaveTimeout);
<a href="chapter_edit.php?id=<?= $next_chapter['id'] ?>" role="button" class="secondary" style="padding: 2px 4px;"> window.tinymceSaveTimeout = setTimeout(function() {
Следующая: <?= e(mb_strimwidth($next_chapter['title'], 0, 30, '...')) ?> ➡️ if (typeof autoSave === 'function') {
</a> autoSave();
<?php endif; ?> }
</div> }, 2000);
</div> });
<?php endif; ?>
// Обработка вставки текста
<script> editor.on('paste', function (e) {
// Обработчик для кнопки предпросмотра // Нормализуем вставленный текст
document.getElementById('preview-button').addEventListener('click', function() { setTimeout(function() {
// Обновляем содержимое для предпросмотра var content = editor.getContent();
document.getElementById('preview-content').value = document.getElementById('content').value; // Убеждаемся, что контент имеет правильную структуру абзацев
document.getElementById('preview-title').value = document.getElementById('title').value || 'Новая глава'; editor.setContent(content);
}, 100);
// Отправляем форму предпросмотра });
document.getElementById('preview-form').submit(); }
}); });
</script> </script>
<?php else: ?>
<script src="assets/js/markdown-editor.js"></script> <!-- Markdown редактор (существующий) -->
<?php if ($is_edit): ?> <textarea name="content" id="content"
<script src="assets/js/autosave.js"></script> placeholder="Начните писать вашу главу здесь..."
<?php endif; ?> rows="15"
style="width: 100%; font-family: monospace;"><?= e($chapter['content'] ?? $_POST['content'] ?? '') ?></textarea>
<script src="/assets/js/markdown-editor.js"></script>
<?php if ($is_edit): ?>
<script src="/assets/js/autosave.js"></script>
<?php endif; ?>
<?php endif; ?>
</div>
</form>
<div class="button-group">
<button type="submit" form="main-form" class="contrast">
<?= $is_edit ? '💾 Сохранить изменения' : '📝 Создать главу' ?>
</button>
<a href="book_edit.php?id=<?= $book_id ?>" role="button" class="secondary">
Отмена
</a>
<button type="button" class="green-btn" id="preview-button">
👁️ Предпросмотр
</button>
</div>
<!-- Форма для предпросмотра -->
<form method="post" action="preview.php" target="_blank" id="preview-form" style="display: none;">
<input type="hidden" name="content" id="preview-content">
<input type="hidden" name="title" id="preview-title" value="<?= e($chapter['title'] ?? 'Новая глава') ?>">
<input type="hidden" name="editor_type" id="preview-editor-type" value="<?= e($book['editor_type'] ?? 'markdown') ?>">
</form>
<?php if ($is_edit): ?>
<div class="button-group">
<a href="chapter_edit.php?book_id=<?= $book_id ?>" role="button">
Новая глава
</a>
<form method="post" action="chapter_delete.php" style="flex: 1;" onsubmit="return confirm('Вы уверены, что хотите удалить эту главу? Это действие нельзя отменить.');">
<input type="hidden" name="chapter_id" value="<?= $chapter_id ?>">
<input type="hidden" name="csrf_token" value="<?= generate_csrf_token() ?>">
<button type="submit" class="secondary delete-btn">
🗑️ Удалить
</button>
</form>
</div>
<?php endif; ?>
<?php if ($is_edit): ?>
<div style="margin-top: 3rem;">
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
<?php
// Получаем все главы книги для навигации
$chapters = $chapterModel->findByBook($book_id);
$current_index = null;
// Находим индекс текущей главы
foreach ($chapters as $index => $chap) {
if ($chap['id'] == $chapter_id) {
$current_index = $index;
break;
}
}
if ($current_index !== null && $current_index > 0):
$prev_chapter = $chapters[$current_index - 1];
?>
<a href="chapter_edit.php?id=<?= $prev_chapter['id'] ?>" role="button" class="secondary" style="padding: 2px 4px;">
⬅️ Предыдущая: <?= e(mb_strimwidth($prev_chapter['title'], 0, 30, '...')) ?>
</a>
<?php endif; ?>
<?php if ($current_index !== null && $current_index < count($chapters) - 1):
$next_chapter = $chapters[$current_index + 1];
?>
<a href="chapter_edit.php?id=<?= $next_chapter['id'] ?>" role="button" class="secondary" style="padding: 2px 4px;">
Следующая: <?= e(mb_strimwidth($next_chapter['title'], 0, 30, '...')) ?> ➡️
</a>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<script>
// Обработчик для кнопки предпросмотра
document.getElementById('preview-button').addEventListener('click', function() {
// Обновляем содержимое для предпросмотра
document.getElementById('preview-content').value = document.getElementById('content').value;
document.getElementById('preview-title').value = document.getElementById('title').value || 'Новая глава';
// Отправляем форму предпросмотра
document.getElementById('preview-form').submit();
});
</script>
<?php include 'views/footer.php'; ?> <?php include 'views/footer.php'; ?>

View File

@ -1,120 +1,120 @@
<?php <?php
require_once 'config/config.php'; require_once 'config/config.php';
require_login(); require_login();
$user_id = $_SESSION['user_id']; $user_id = $_SESSION['user_id'];
$book_id = $_GET['book_id'] ?? null; $book_id = $_GET['book_id'] ?? null;
if (!$book_id) { if (!$book_id) {
$_SESSION['error'] = "Не указана книга"; $_SESSION['error'] = "Не указана книга";
redirect('books.php'); redirect('books.php');
} }
$bookModel = new Book($pdo); $bookModel = new Book($pdo);
$chapterModel = new Chapter($pdo); $chapterModel = new Chapter($pdo);
if (!$bookModel->userOwnsBook($book_id, $user_id)) { if (!$bookModel->userOwnsBook($book_id, $user_id)) {
$_SESSION['error'] = "У вас нет доступа к этой книге"; $_SESSION['error'] = "У вас нет доступа к этой книге";
redirect('books.php'); redirect('books.php');
} }
// Получаем информацию о книге и главах // Получаем информацию о книге и главах
$book = $bookModel->findById($book_id); $book = $bookModel->findById($book_id);
$chapters = $chapterModel->findByBook($book_id); $chapters = $chapterModel->findByBook($book_id);
$page_title = "Главы книги: " . e($book['title']); $page_title = "Главы книги: " . e($book['title']);
include 'views/header.php'; include 'views/header.php';
?> ?>
<div style="margin-bottom: 1rem;"> <div style="margin-bottom: 1rem;">
<h1 style="margin: 0 0 0.5rem 0; font-size: 1.5rem;">Главы книги: <?= e($book['title']) ?></h1> <h1 style="margin: 0 0 0.5rem 0; font-size: 1.5rem;">Главы книги: <?= e($book['title']) ?></h1>
<div style="display: flex; gap: 5px; flex-wrap: wrap;"> <div style="display: flex; gap: 5px; flex-wrap: wrap;">
<a href="chapter_edit.php?book_id=<?= $book_id ?>" class="adaptive-button"> Новая глава</a> <a href="chapter_edit.php?book_id=<?= $book_id ?>" class="adaptive-button"> Новая глава</a>
<a href="book_edit.php?id=<?= $book_id ?>" class="adaptive-button secondary">✏️ Редактировать книгу</a> <a href="book_edit.php?id=<?= $book_id ?>" class="adaptive-button secondary">✏️ Редактировать книгу</a>
<a href="view_book.php?share_token=<?= $book['share_token'] ?>" class="adaptive-button secondary" target="_blank">👁️ Просмотреть книгу</a> <a href="view_book.php?share_token=<?= $book['share_token'] ?>" class="adaptive-button secondary" target="_blank">👁️ Просмотреть книгу</a>
<a href="books.php" class="adaptive-button secondary">📚 Все книги</a> <a href="books.php" class="adaptive-button secondary">📚 Все книги</a>
</div> </div>
</div> </div>
<?php if (isset($_SESSION['success'])): ?> <?php if (isset($_SESSION['success'])): ?>
<div class="alert alert-success"> <div class="alert alert-success">
<?= e($_SESSION['success']) ?> <?= e($_SESSION['success']) ?>
<?php unset($_SESSION['success']); ?> <?php unset($_SESSION['success']); ?>
</div> </div>
<?php endif; ?> <?php endif; ?>
<?php if (isset($_SESSION['error'])): ?> <?php if (isset($_SESSION['error'])): ?>
<div class="alert alert-error"> <div class="alert alert-error">
<?= e($_SESSION['error']) ?> <?= e($_SESSION['error']) ?>
<?php unset($_SESSION['error']); ?> <?php unset($_SESSION['error']); ?>
</div> </div>
<?php endif; ?> <?php endif; ?>
<?php if (empty($chapters)): ?> <?php if (empty($chapters)): ?>
<div style="text-align: center; padding: 2rem; background: #f9f9f9; border-radius: 5px; margin-top: 1rem;"> <div style="text-align: center; padding: 2rem; background: #f9f9f9; border-radius: 5px; margin-top: 1rem;">
<h3>В этой книге пока нет глав</h3> <h3>В этой книге пока нет глав</h3>
<p>Создайте первую главу для вашей книги</p> <p>Создайте первую главу для вашей книги</p>
<a href="chapter_edit.php?book_id=<?= $book_id ?>" class="adaptive-button">📝 Создать первую главу</a> <a href="chapter_edit.php?book_id=<?= $book_id ?>" class="adaptive-button">📝 Создать первую главу</a>
</div> </div>
<?php else: ?> <?php else: ?>
<div style="overflow-x: auto; margin-top: 1rem;"> <div style="overflow-x: auto; margin-top: 1rem;">
<table class="compact-table"> <table class="compact-table">
<thead> <thead>
<tr> <tr>
<th style="width: 5%;"></th> <th style="width: 5%;"></th>
<th style="width: 40%;">Название главы</th> <th style="width: 40%;">Название главы</th>
<th style="width: 15%;">Статус</th> <th style="width: 15%;">Статус</th>
<th style="width: 10%;">Слов</th> <th style="width: 10%;">Слов</th>
<th style="width: 20%;">Обновлено</th> <th style="width: 20%;">Обновлено</th>
<th style="width: 10%;">Действия</th> <th style="width: 10%;">Действия</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<?php foreach ($chapters as $index => $chapter): ?> <?php foreach ($chapters as $index => $chapter): ?>
<tr> <tr>
<td><?= $index + 1 ?></td> <td><?= $index + 1 ?></td>
<td> <td>
<strong><?= e($chapter['title']) ?></strong> <strong><?= e($chapter['title']) ?></strong>
<?php if ($chapter['description']): ?> <?php if ($chapter['description']): ?>
<br><small style="color: #666;"><?= e(mb_strimwidth($chapter['description'], 0, 100, '...')) ?></small> <br><small style="color: #666;"><?= e(mb_strimwidth($chapter['description'], 0, 100, '...')) ?></small>
<?php endif; ?> <?php endif; ?>
</td> </td>
<td> <td>
<span style="color: <?= $chapter['status'] == 'published' ? 'green' : 'orange' ?>"> <span style="color: <?= $chapter['status'] == 'published' ? 'green' : 'orange' ?>">
<?= $chapter['status'] == 'published' ? '✅ Опубликована' : '📝 Черновик' ?> <?= $chapter['status'] == 'published' ? '✅ Опубликована' : '📝 Черновик' ?>
</span> </span>
</td> </td>
<td><?= $chapter['word_count'] ?></td> <td><?= $chapter['word_count'] ?></td>
<td> <td>
<small><?= date('d.m.Y H:i', strtotime($chapter['updated_at'])) ?></small> <small><?= date('d.m.Y H:i', strtotime($chapter['updated_at'])) ?></small>
</td> </td>
<td> <td>
<div style="display: flex; gap: 3px; flex-wrap: wrap;"> <div style="display: flex; gap: 3px; flex-wrap: wrap;">
<a href="chapter_edit.php?id=<?= $chapter['id'] ?>" class="compact-button secondary" title="Редактировать"> <a href="chapter_edit.php?id=<?= $chapter['id'] ?>" class="compact-button secondary" title="Редактировать">
✏️ ✏️
</a> </a>
<form method="post" action="chapter_delete.php" style="display: inline;" onsubmit="return confirm('Вы уверены, что хотите удалить эту главу? Это действие нельзя отменить.');"> <form method="post" action="chapter_delete.php" style="display: inline;" onsubmit="return confirm('Вы уверены, что хотите удалить эту главу? Это действие нельзя отменить.');">
<input type="hidden" name="chapter_id" value="<?= $chapter['id'] ?>"> <input type="hidden" name="chapter_id" value="<?= $chapter['id'] ?>">
<input type="hidden" name="csrf_token" value="<?= generate_csrf_token() ?>"> <input type="hidden" name="csrf_token" value="<?= generate_csrf_token() ?>">
<button type="submit" class="compact-button secondary" style="background: #ff4444; border-color: #ff4444; color: white;" title="Удалить"> <button type="submit" class="compact-button secondary" style="background: #ff4444; border-color: #ff4444; color: white;" title="Удалить">
🗑️ 🗑️
</button> </button>
</form> </form>
</div> </div>
</td> </td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
</tbody> </tbody>
</table> </table>
</div> </div>
<div style="margin-top: 1rem; padding: 0.5rem; background: #f5f5f5; border-radius: 3px;"> <div style="margin-top: 1rem; padding: 0.5rem; background: #f5f5f5; border-radius: 3px;">
<strong>Статистика:</strong> <strong>Статистика:</strong>
Всего глав: <?= count($chapters) ?> | Всего глав: <?= count($chapters) ?> |
Всего слов: <?= array_sum(array_column($chapters, 'word_count')) ?> | Всего слов: <?= array_sum(array_column($chapters, 'word_count')) ?> |
Опубликовано: <?= count(array_filter($chapters, function($ch) { return $ch['status'] == 'published'; })) ?> Опубликовано: <?= count(array_filter($chapters, function($ch) { return $ch['status'] == 'published'; })) ?>
</div> </div>
<?php endif; ?> <?php endif; ?>
<?php include 'views/footer.php'; ?> <?php include 'views/footer.php'; ?>

View File

@ -1,6 +1,6 @@
{ {
"require": { "require": {
"phpoffice/phpword": "^1.0", "phpoffice/phpword": "^1.0",
"tecnickcom/tcpdf": "^6.6" "tecnickcom/tcpdf": "^6.6"
} }
} }

578
composer.lock generated
View File

@ -1,289 +1,289 @@
{ {
"_readme": [ "_readme": [
"This file locks the dependencies of your project to a known state", "This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "493a3be12648bbe702ed126df05ead04", "content-hash": "493a3be12648bbe702ed126df05ead04",
"packages": [ "packages": [
{ {
"name": "cybermonde/odtphp", "name": "cybermonde/odtphp",
"version": "v1.7", "version": "v1.7",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/cybermonde/odtphp.git", "url": "https://github.com/cybermonde/odtphp.git",
"reference": "23aba70923ca3c07af15a5600f4072751c1b4a36" "reference": "23aba70923ca3c07af15a5600f4072751c1b4a36"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/cybermonde/odtphp/zipball/23aba70923ca3c07af15a5600f4072751c1b4a36", "url": "https://api.github.com/repos/cybermonde/odtphp/zipball/23aba70923ca3c07af15a5600f4072751c1b4a36",
"reference": "23aba70923ca3c07af15a5600f4072751c1b4a36", "reference": "23aba70923ca3c07af15a5600f4072751c1b4a36",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": ">=5.2.4" "php": ">=5.2.4"
}, },
"type": "library", "type": "library",
"autoload": { "autoload": {
"classmap": [ "classmap": [
"library" "library"
] ]
}, },
"notification-url": "https://packagist.org/downloads/", "notification-url": "https://packagist.org/downloads/",
"license": [ "license": [
"GPL" "GPL"
], ],
"description": "ODT document generator", "description": "ODT document generator",
"homepage": "https://github.com/cybermonde/odtphp", "homepage": "https://github.com/cybermonde/odtphp",
"keywords": [ "keywords": [
"odt", "odt",
"php" "php"
], ],
"support": { "support": {
"issues": "https://github.com/cybermonde/odtphp/issues", "issues": "https://github.com/cybermonde/odtphp/issues",
"source": "https://github.com/cybermonde/odtphp/tree/v1.7" "source": "https://github.com/cybermonde/odtphp/tree/v1.7"
}, },
"time": "2015-06-02T07:28:25+00:00" "time": "2015-06-02T07:28:25+00:00"
}, },
{ {
"name": "phpoffice/math", "name": "phpoffice/math",
"version": "0.3.0", "version": "0.3.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/PHPOffice/Math.git", "url": "https://github.com/PHPOffice/Math.git",
"reference": "fc31c8f57a7a81f962cbf389fd89f4d9d06fc99a" "reference": "fc31c8f57a7a81f962cbf389fd89f4d9d06fc99a"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/PHPOffice/Math/zipball/fc31c8f57a7a81f962cbf389fd89f4d9d06fc99a", "url": "https://api.github.com/repos/PHPOffice/Math/zipball/fc31c8f57a7a81f962cbf389fd89f4d9d06fc99a",
"reference": "fc31c8f57a7a81f962cbf389fd89f4d9d06fc99a", "reference": "fc31c8f57a7a81f962cbf389fd89f4d9d06fc99a",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"ext-dom": "*", "ext-dom": "*",
"ext-xml": "*", "ext-xml": "*",
"php": "^7.1|^8.0" "php": "^7.1|^8.0"
}, },
"require-dev": { "require-dev": {
"phpstan/phpstan": "^0.12.88 || ^1.0.0", "phpstan/phpstan": "^0.12.88 || ^1.0.0",
"phpunit/phpunit": "^7.0 || ^9.0" "phpunit/phpunit": "^7.0 || ^9.0"
}, },
"type": "library", "type": "library",
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"PhpOffice\\Math\\": "src/Math/" "PhpOffice\\Math\\": "src/Math/"
} }
}, },
"notification-url": "https://packagist.org/downloads/", "notification-url": "https://packagist.org/downloads/",
"license": [ "license": [
"MIT" "MIT"
], ],
"authors": [ "authors": [
{ {
"name": "Progi1984", "name": "Progi1984",
"homepage": "https://lefevre.dev" "homepage": "https://lefevre.dev"
} }
], ],
"description": "Math - Manipulate Math Formula", "description": "Math - Manipulate Math Formula",
"homepage": "https://phpoffice.github.io/Math/", "homepage": "https://phpoffice.github.io/Math/",
"keywords": [ "keywords": [
"MathML", "MathML",
"officemathml", "officemathml",
"php" "php"
], ],
"support": { "support": {
"issues": "https://github.com/PHPOffice/Math/issues", "issues": "https://github.com/PHPOffice/Math/issues",
"source": "https://github.com/PHPOffice/Math/tree/0.3.0" "source": "https://github.com/PHPOffice/Math/tree/0.3.0"
}, },
"time": "2025-05-29T08:31:49+00:00" "time": "2025-05-29T08:31:49+00:00"
}, },
{ {
"name": "phpoffice/phpword", "name": "phpoffice/phpword",
"version": "1.4.0", "version": "1.4.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/PHPOffice/PHPWord.git", "url": "https://github.com/PHPOffice/PHPWord.git",
"reference": "6d75328229bc93790b37e93741adf70646cea958" "reference": "6d75328229bc93790b37e93741adf70646cea958"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/PHPOffice/PHPWord/zipball/6d75328229bc93790b37e93741adf70646cea958", "url": "https://api.github.com/repos/PHPOffice/PHPWord/zipball/6d75328229bc93790b37e93741adf70646cea958",
"reference": "6d75328229bc93790b37e93741adf70646cea958", "reference": "6d75328229bc93790b37e93741adf70646cea958",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"ext-dom": "*", "ext-dom": "*",
"ext-gd": "*", "ext-gd": "*",
"ext-json": "*", "ext-json": "*",
"ext-xml": "*", "ext-xml": "*",
"ext-zip": "*", "ext-zip": "*",
"php": "^7.1|^8.0", "php": "^7.1|^8.0",
"phpoffice/math": "^0.3" "phpoffice/math": "^0.3"
}, },
"require-dev": { "require-dev": {
"dompdf/dompdf": "^2.0 || ^3.0", "dompdf/dompdf": "^2.0 || ^3.0",
"ext-libxml": "*", "ext-libxml": "*",
"friendsofphp/php-cs-fixer": "^3.3", "friendsofphp/php-cs-fixer": "^3.3",
"mpdf/mpdf": "^7.0 || ^8.0", "mpdf/mpdf": "^7.0 || ^8.0",
"phpmd/phpmd": "^2.13", "phpmd/phpmd": "^2.13",
"phpstan/phpstan": "^0.12.88 || ^1.0.0", "phpstan/phpstan": "^0.12.88 || ^1.0.0",
"phpstan/phpstan-phpunit": "^1.0 || ^2.0", "phpstan/phpstan-phpunit": "^1.0 || ^2.0",
"phpunit/phpunit": ">=7.0", "phpunit/phpunit": ">=7.0",
"symfony/process": "^4.4 || ^5.0", "symfony/process": "^4.4 || ^5.0",
"tecnickcom/tcpdf": "^6.5" "tecnickcom/tcpdf": "^6.5"
}, },
"suggest": { "suggest": {
"dompdf/dompdf": "Allows writing PDF", "dompdf/dompdf": "Allows writing PDF",
"ext-xmlwriter": "Allows writing OOXML and ODF", "ext-xmlwriter": "Allows writing OOXML and ODF",
"ext-xsl": "Allows applying XSL style sheet to headers, to main document part, and to footers of an OOXML template" "ext-xsl": "Allows applying XSL style sheet to headers, to main document part, and to footers of an OOXML template"
}, },
"type": "library", "type": "library",
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"PhpOffice\\PhpWord\\": "src/PhpWord" "PhpOffice\\PhpWord\\": "src/PhpWord"
} }
}, },
"notification-url": "https://packagist.org/downloads/", "notification-url": "https://packagist.org/downloads/",
"license": [ "license": [
"LGPL-3.0-only" "LGPL-3.0-only"
], ],
"authors": [ "authors": [
{ {
"name": "Mark Baker" "name": "Mark Baker"
}, },
{ {
"name": "Gabriel Bull", "name": "Gabriel Bull",
"email": "me@gabrielbull.com", "email": "me@gabrielbull.com",
"homepage": "http://gabrielbull.com/" "homepage": "http://gabrielbull.com/"
}, },
{ {
"name": "Franck Lefevre", "name": "Franck Lefevre",
"homepage": "https://rootslabs.net/blog/" "homepage": "https://rootslabs.net/blog/"
}, },
{ {
"name": "Ivan Lanin", "name": "Ivan Lanin",
"homepage": "http://ivan.lanin.org" "homepage": "http://ivan.lanin.org"
}, },
{ {
"name": "Roman Syroeshko", "name": "Roman Syroeshko",
"homepage": "http://ru.linkedin.com/pub/roman-syroeshko/34/a53/994/" "homepage": "http://ru.linkedin.com/pub/roman-syroeshko/34/a53/994/"
}, },
{ {
"name": "Antoine de Troostembergh" "name": "Antoine de Troostembergh"
} }
], ],
"description": "PHPWord - A pure PHP library for reading and writing word processing documents (OOXML, ODF, RTF, HTML, PDF)", "description": "PHPWord - A pure PHP library for reading and writing word processing documents (OOXML, ODF, RTF, HTML, PDF)",
"homepage": "https://phpoffice.github.io/PHPWord/", "homepage": "https://phpoffice.github.io/PHPWord/",
"keywords": [ "keywords": [
"ISO IEC 29500", "ISO IEC 29500",
"OOXML", "OOXML",
"Office Open XML", "Office Open XML",
"OpenDocument", "OpenDocument",
"OpenXML", "OpenXML",
"PhpOffice", "PhpOffice",
"PhpWord", "PhpWord",
"Rich Text Format", "Rich Text Format",
"WordprocessingML", "WordprocessingML",
"doc", "doc",
"docx", "docx",
"html", "html",
"odf", "odf",
"odt", "odt",
"office", "office",
"pdf", "pdf",
"php", "php",
"reader", "reader",
"rtf", "rtf",
"template", "template",
"template processor", "template processor",
"word", "word",
"writer" "writer"
], ],
"support": { "support": {
"issues": "https://github.com/PHPOffice/PHPWord/issues", "issues": "https://github.com/PHPOffice/PHPWord/issues",
"source": "https://github.com/PHPOffice/PHPWord/tree/1.4.0" "source": "https://github.com/PHPOffice/PHPWord/tree/1.4.0"
}, },
"time": "2025-06-05T10:32:36+00:00" "time": "2025-06-05T10:32:36+00:00"
}, },
{ {
"name": "tecnickcom/tcpdf", "name": "tecnickcom/tcpdf",
"version": "6.10.0", "version": "6.10.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/tecnickcom/TCPDF.git", "url": "https://github.com/tecnickcom/TCPDF.git",
"reference": "ca5b6de294512145db96bcbc94e61696599c391d" "reference": "ca5b6de294512145db96bcbc94e61696599c391d"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/tecnickcom/TCPDF/zipball/ca5b6de294512145db96bcbc94e61696599c391d", "url": "https://api.github.com/repos/tecnickcom/TCPDF/zipball/ca5b6de294512145db96bcbc94e61696599c391d",
"reference": "ca5b6de294512145db96bcbc94e61696599c391d", "reference": "ca5b6de294512145db96bcbc94e61696599c391d",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"ext-curl": "*", "ext-curl": "*",
"php": ">=7.1.0" "php": ">=7.1.0"
}, },
"type": "library", "type": "library",
"autoload": { "autoload": {
"classmap": [ "classmap": [
"config", "config",
"include", "include",
"tcpdf.php", "tcpdf.php",
"tcpdf_barcodes_1d.php", "tcpdf_barcodes_1d.php",
"tcpdf_barcodes_2d.php", "tcpdf_barcodes_2d.php",
"include/tcpdf_colors.php", "include/tcpdf_colors.php",
"include/tcpdf_filters.php", "include/tcpdf_filters.php",
"include/tcpdf_font_data.php", "include/tcpdf_font_data.php",
"include/tcpdf_fonts.php", "include/tcpdf_fonts.php",
"include/tcpdf_images.php", "include/tcpdf_images.php",
"include/tcpdf_static.php", "include/tcpdf_static.php",
"include/barcodes/datamatrix.php", "include/barcodes/datamatrix.php",
"include/barcodes/pdf417.php", "include/barcodes/pdf417.php",
"include/barcodes/qrcode.php" "include/barcodes/qrcode.php"
] ]
}, },
"notification-url": "https://packagist.org/downloads/", "notification-url": "https://packagist.org/downloads/",
"license": [ "license": [
"LGPL-3.0-or-later" "LGPL-3.0-or-later"
], ],
"authors": [ "authors": [
{ {
"name": "Nicola Asuni", "name": "Nicola Asuni",
"email": "info@tecnick.com", "email": "info@tecnick.com",
"role": "lead" "role": "lead"
} }
], ],
"description": "TCPDF is a PHP class for generating PDF documents and barcodes.", "description": "TCPDF is a PHP class for generating PDF documents and barcodes.",
"homepage": "http://www.tcpdf.org/", "homepage": "http://www.tcpdf.org/",
"keywords": [ "keywords": [
"PDFD32000-2008", "PDFD32000-2008",
"TCPDF", "TCPDF",
"barcodes", "barcodes",
"datamatrix", "datamatrix",
"pdf", "pdf",
"pdf417", "pdf417",
"qrcode" "qrcode"
], ],
"support": { "support": {
"issues": "https://github.com/tecnickcom/TCPDF/issues", "issues": "https://github.com/tecnickcom/TCPDF/issues",
"source": "https://github.com/tecnickcom/TCPDF/tree/6.10.0" "source": "https://github.com/tecnickcom/TCPDF/tree/6.10.0"
}, },
"funding": [ "funding": [
{ {
"url": "https://www.paypal.com/donate/?hosted_button_id=NZUEC5XS8MFBJ", "url": "https://www.paypal.com/donate/?hosted_button_id=NZUEC5XS8MFBJ",
"type": "custom" "type": "custom"
} }
], ],
"time": "2025-05-27T18:02:28+00:00" "time": "2025-05-27T18:02:28+00:00"
} }
], ],
"packages-dev": [], "packages-dev": [],
"aliases": [], "aliases": [],
"minimum-stability": "stable", "minimum-stability": "stable",
"stability-flags": {}, "stability-flags": {},
"prefer-stable": false, "prefer-stable": false,
"prefer-lowest": false, "prefer-lowest": false,
"platform": {}, "platform": {},
"platform-dev": {}, "platform-dev": {},
"plugin-api-version": "2.9.0" "plugin-api-version": "2.9.0"
} }

View File

@ -1,191 +1,191 @@
<?php <?php
require_once 'config/config.php'; require_once 'config/config.php';
require_login(); require_login();
$user_id = $_SESSION['user_id']; $user_id = $_SESSION['user_id'];
$bookModel = new Book($pdo); $bookModel = new Book($pdo);
$seriesModel = new Series($pdo); $seriesModel = new Series($pdo);
$books = $bookModel->findByUser($user_id); $books = $bookModel->findByUser($user_id);
$series = $seriesModel->findByUser($user_id); $series = $seriesModel->findByUser($user_id);
// Статистика по книгам // Статистика по книгам
$total_chapters = 0; $total_chapters = 0;
$total_words = 0; $total_words = 0;
foreach ($books as $book) { foreach ($books as $book) {
$total_chapters += $book['chapter_count']; $total_chapters += $book['chapter_count'];
$total_words += $book['total_words']; $total_words += $book['total_words'];
} }
// Статистика по сериям // Статистика по сериям
$series_stats = [ $series_stats = [
'total_series' => count($series), 'total_series' => count($series),
'series_with_books' => 0, 'series_with_books' => 0,
'total_books_in_series' => 0 'total_books_in_series' => 0
]; ];
foreach ($series as $ser) { foreach ($series as $ser) {
$series_books = $seriesModel->getBooksInSeries($ser['id']); $series_books = $seriesModel->getBooksInSeries($ser['id']);
$series_stats['total_books_in_series'] += count($series_books); $series_stats['total_books_in_series'] += count($series_books);
if (count($series_books) > 0) { if (count($series_books) > 0) {
$series_stats['series_with_books']++; $series_stats['series_with_books']++;
} }
} }
$page_title = "Панель управления"; $page_title = "Панель управления";
include 'views/header.php'; include 'views/header.php';
?> ?>
<h1>Добро пожаловать, <?= e($_SESSION['display_name']) ?>!</h1> <h1>Добро пожаловать, <?= e($_SESSION['display_name']) ?>!</h1>
<div style="margin-bottom: 1rem;"> <div style="margin-bottom: 1rem;">
<a href="profile.php" class="adaptive-button secondary">✏️ Редактировать профиль</a> <a href="profile.php" class="adaptive-button secondary">✏️ Редактировать профиль</a>
</div> </div>
<div class="grid"> <div class="grid">
<article> <article>
<h2>📚 Мои книги</h2> <h2>📚 Мои книги</h2>
<p>Управляйте вашими книгами и главами</p> <p>Управляйте вашими книгами и главами</p>
<div class="dashboard-buttons"> <div class="dashboard-buttons">
<a href="books.php" role="button" class="dashboard-button"> <a href="books.php" role="button" class="dashboard-button">
Мои книги (<?= count($books) ?>) Мои книги (<?= count($books) ?>)
</a> </a>
<a href="book_edit.php" role="button" class="dashboard-button new"> <a href="book_edit.php" role="button" class="dashboard-button new">
Новая книга Новая книга
</a> </a>
</div> </div>
</article> </article>
<article> <article>
<h2>📊 Статистика</h2> <h2>📊 Статистика</h2>
<div class="stats-list"> <div class="stats-list">
<p><strong>Книг:</strong> <?= count($books) ?></p> <p><strong>Книг:</strong> <?= count($books) ?></p>
<p><strong>Глав:</strong> <?= $total_chapters ?></p> <p><strong>Глав:</strong> <?= $total_chapters ?></p>
<p><strong>Всего слов:</strong> <?= $total_words ?></p> <p><strong>Всего слов:</strong> <?= $total_words ?></p>
<?php if ($total_words > 0): ?> <?php if ($total_words > 0): ?>
<p><strong>Средняя глава:</strong> <?= round($total_words / max(1, $total_chapters)) ?> слов</p> <p><strong>Средняя глава:</strong> <?= round($total_words / max(1, $total_chapters)) ?> слов</p>
<?php endif; ?> <?php endif; ?>
</div> </div>
</article> </article>
<article> <article>
<h2>📖 Мои серии</h2> <h2>📖 Мои серии</h2>
<p>Управляйте сериями книг</p> <p>Управляйте сериями книг</p>
<div class="dashboard-buttons"> <div class="dashboard-buttons">
<a href="series.php" role="button" class="dashboard-button"> <a href="series.php" role="button" class="dashboard-button">
Мои серии (<?= $series_stats['total_series'] ?>) Мои серии (<?= $series_stats['total_series'] ?>)
</a> </a>
<a href="series_edit.php" role="button" class="dashboard-button new"> <a href="series_edit.php" role="button" class="dashboard-button new">
Новая серия Новая серия
</a> </a>
</div> </div>
<?php if ($series_stats['total_series'] > 0): ?> <?php if ($series_stats['total_series'] > 0): ?>
<div class="series-stats"> <div class="series-stats">
<p><strong>Книг в сериях:</strong> <?= $series_stats['total_books_in_series'] ?></p> <p><strong>Книг в сериях:</strong> <?= $series_stats['total_books_in_series'] ?></p>
<p><strong>Заполненных серий:</strong> <?= $series_stats['series_with_books'] ?></p> <p><strong>Заполненных серий:</strong> <?= $series_stats['series_with_books'] ?></p>
</div> </div>
<?php endif; ?> <?php endif; ?>
</article> </article>
</div> </div>
<?php if (!empty($books)): ?> <?php if (!empty($books)): ?>
<div class="dashboard-section"> <div class="dashboard-section">
<h2>Недавние книги</h2> <h2>Недавние книги</h2>
<div class="grid"> <div class="grid">
<?php foreach (array_slice($books, 0, 3) as $book): ?> <?php foreach (array_slice($books, 0, 3) as $book): ?>
<article class="dashboard-item"> <article class="dashboard-item">
<h4> <h4>
<?= e($book['title']) ?> <?= e($book['title']) ?>
<?php if ($book['series_id']): ?> <?php if ($book['series_id']): ?>
<?php <?php
$series_stmt = $pdo->prepare("SELECT title FROM series WHERE id = ?"); $series_stmt = $pdo->prepare("SELECT title FROM series WHERE id = ?");
$series_stmt->execute([$book['series_id']]); $series_stmt->execute([$book['series_id']]);
$series_title = $series_stmt->fetch()['title'] ?? ''; $series_title = $series_stmt->fetch()['title'] ?? '';
?> ?>
<?php if ($series_title): ?> <?php if ($series_title): ?>
<br><small style="color: #007bff;">📚 <?= e($series_title) ?></small> <br><small style="color: #007bff;">📚 <?= e($series_title) ?></small>
<?php endif; ?> <?php endif; ?>
<?php endif; ?> <?php endif; ?>
</h4> </h4>
<p>Глав: <?= $book['chapter_count'] ?> | Слов: <?= $book['total_words'] ?></p> <p>Глав: <?= $book['chapter_count'] ?> | Слов: <?= $book['total_words'] ?></p>
<div class="action-buttons"> <div class="action-buttons">
<a href="book_edit.php?id=<?= $book['id'] ?>" role="button" class="compact-button secondary"> <a href="book_edit.php?id=<?= $book['id'] ?>" role="button" class="compact-button secondary">
Редактировать Редактировать
</a> </a>
<a href="chapters.php?book_id=<?= $book['id'] ?>" role="button" class="compact-button secondary"> <a href="chapters.php?book_id=<?= $book['id'] ?>" role="button" class="compact-button secondary">
Главы Главы
</a> </a>
<a href="view_book.php?share_token=<?= $book['share_token'] ?>" role="button" class="compact-button secondary" target="_blank"> <a href="view_book.php?share_token=<?= $book['share_token'] ?>" role="button" class="compact-button secondary" target="_blank">
Просмотр Просмотр
</a> </a>
</div> </div>
</article> </article>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>
<?php if (count($books) > 3): ?> <?php if (count($books) > 3): ?>
<div style="text-align: center; margin-top: 1rem;"> <div style="text-align: center; margin-top: 1rem;">
<a href="books.php" role="button" class="secondary">📚 Показать все книги (<?= count($books) ?>)</a> <a href="books.php" role="button" class="secondary">📚 Показать все книги (<?= count($books) ?>)</a>
</div> </div>
<?php endif; ?> <?php endif; ?>
</div> </div>
<?php endif; ?> <?php endif; ?>
<?php if (!empty($series)): ?> <?php if (!empty($series)): ?>
<div class="dashboard-section"> <div class="dashboard-section">
<h2>Недавние серии</h2> <h2>Недавние серии</h2>
<div class="grid"> <div class="grid">
<?php foreach (array_slice($series, 0, 3) as $ser): ?> <?php foreach (array_slice($series, 0, 3) as $ser): ?>
<article class="dashboard-item"> <article class="dashboard-item">
<h4><?= e($ser['title']) ?></h4> <h4><?= e($ser['title']) ?></h4>
<?php <?php
$books_in_series = $seriesModel->getBooksInSeries($ser['id']); $books_in_series = $seriesModel->getBooksInSeries($ser['id']);
$series_words = 0; $series_words = 0;
$series_chapters = 0; $series_chapters = 0;
foreach ($books_in_series as $book) { foreach ($books_in_series as $book) {
$book_stats = $bookModel->getBookStats($book['id']); $book_stats = $bookModel->getBookStats($book['id']);
$series_words += $book_stats['total_words'] ?? 0; $series_words += $book_stats['total_words'] ?? 0;
$series_chapters += $book_stats['chapter_count'] ?? 0; $series_chapters += $book_stats['chapter_count'] ?? 0;
} }
?> ?>
<p>Книг: <?= count($books_in_series) ?> | Глав: <?= $series_chapters ?> | Слов: <?= $series_words ?></p> <p>Книг: <?= count($books_in_series) ?> | Глав: <?= $series_chapters ?> | Слов: <?= $series_words ?></p>
<div class="action-buttons"> <div class="action-buttons">
<a href="series_edit.php?id=<?= $ser['id'] ?>" role="button" class="compact-button secondary"> <a href="series_edit.php?id=<?= $ser['id'] ?>" role="button" class="compact-button secondary">
Редактировать Редактировать
</a> </a>
<a href="view_series.php?id=<?= $ser['id'] ?>" role="button" class="compact-button secondary" target="_blank"> <a href="view_series.php?id=<?= $ser['id'] ?>" role="button" class="compact-button secondary" target="_blank">
Просмотр Просмотр
</a> </a>
</div> </div>
</article> </article>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>
<?php if (count($series) > 3): ?> <?php if (count($series) > 3): ?>
<div style="text-align: center; margin-top: 1rem;"> <div style="text-align: center; margin-top: 1rem;">
<a href="series.php" role="button" class="secondary">📖 Показать все серии (<?= count($series) ?>)</a> <a href="series.php" role="button" class="secondary">📖 Показать все серии (<?= count($series) ?>)</a>
</div> </div>
<?php endif; ?> <?php endif; ?>
</div> </div>
<?php endif; ?> <?php endif; ?>
<?php if (empty($books) && empty($series)): ?> <?php if (empty($books) && empty($series)): ?>
<div class="welcome-message"> <div class="welcome-message">
<h3>Добро пожаловать в <?= e(APP_NAME) ?>!</h3> <h3>Добро пожаловать в <?= e(APP_NAME) ?>!</h3>
<p>Начните создавать свои литературные произведения</p> <p>Начните создавать свои литературные произведения</p>
<div class="welcome-buttons"> <div class="welcome-buttons">
<a href="book_edit.php" role="button" class="contrast">📖 Создать первую книгу</a> <a href="book_edit.php" role="button" class="contrast">📖 Создать первую книгу</a>
<a href="series_edit.php" role="button" class="secondary">📚 Создать первую серию</a> <a href="series_edit.php" role="button" class="secondary">📚 Создать первую серию</a>
</div> </div>
<div style="margin-top: 1.5rem;"> <div style="margin-top: 1.5rem;">
<a href="profile.php" role="button" class="secondary">✏️ Настроить профиль</a> <a href="profile.php" role="button" class="secondary">✏️ Настроить профиль</a>
</div> </div>
</div> </div>
<?php endif; ?> <?php endif; ?>
<?php include 'views/footer.php'; ?> <?php include 'views/footer.php'; ?>

File diff suppressed because it is too large Load Diff

View File

@ -61,6 +61,7 @@ CREATE TABLE `books` (
`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), `updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
`share_token` varchar(32) DEFAULT NULL, `share_token` varchar(32) DEFAULT NULL,
`published` tinyint(1) NOT NULL DEFAULT 0, `published` tinyint(1) NOT NULL DEFAULT 0,
`editor_type` ENUM('markdown', 'html') DEFAULT 'markdown',
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
UNIQUE KEY `share_token` (`share_token`), UNIQUE KEY `share_token` (`share_token`),
KEY `user_id` (`user_id`), KEY `user_id` (`user_id`),

198
login.php
View File

@ -1,100 +1,100 @@
<?php <?php
require_once 'config/config.php'; require_once 'config/config.php';
// Если пользователь уже авторизован, перенаправляем на dashboard // Если пользователь уже авторизован, перенаправляем на dashboard
if (is_logged_in()) { if (is_logged_in()) {
redirect('dashboard.php'); redirect('dashboard.php');
} }
$error = ''; $error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!verify_csrf_token($_POST['csrf_token'] ?? '')) { if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
$error = "Ошибка безопасности"; $error = "Ошибка безопасности";
} else { } else {
$username = trim($_POST['username'] ?? ''); $username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? ''; $password = $_POST['password'] ?? '';
if (empty($username) || empty($password)) { if (empty($username) || empty($password)) {
$error = 'Пожалуйста, введите имя пользователя и пароль'; $error = 'Пожалуйста, введите имя пользователя и пароль';
} else { } else {
$userModel = new User($pdo); $userModel = new User($pdo);
$user = $userModel->findByUsername($username); $user = $userModel->findByUsername($username);
if ($user && $userModel->verifyPassword($password, $user['password_hash'])) { if ($user && $userModel->verifyPassword($password, $user['password_hash'])) {
if (!$user['is_active']) { if (!$user['is_active']) {
$error = 'Ваш аккаунт деактивирован или ожидает активации администратором.'; $error = 'Ваш аккаунт деактивирован или ожидает активации администратором.';
} else { } else {
// Успешный вход // Успешный вход
$_SESSION['user_id'] = $user['id']; $_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username']; $_SESSION['username'] = $user['username'];
$_SESSION['display_name'] = $user['display_name'] ?: $user['username']; $_SESSION['display_name'] = $user['display_name'] ?: $user['username'];
$_SESSION['avatar'] = $user['avatar'] ?? null; $_SESSION['avatar'] = $user['avatar'] ?? null;
// Обновляем время последнего входа // Обновляем время последнего входа
$userModel->updateLastLogin($user['id']); $userModel->updateLastLogin($user['id']);
$_SESSION['success'] = 'Добро пожаловать, ' . e($user['display_name'] ?: $user['username']) . '!'; $_SESSION['success'] = 'Добро пожаловать, ' . e($user['display_name'] ?: $user['username']) . '!';
redirect('dashboard.php'); redirect('dashboard.php');
} }
} else { } else {
$error = 'Неверное имя пользователя или пароль'; $error = 'Неверное имя пользователя или пароль';
} }
} }
} }
} }
$page_title = 'Вход в систему'; $page_title = 'Вход в систему';
include 'views/header.php'; include 'views/header.php';
?> ?>
<div class="container"> <div class="container">
<h1>Вход в систему</h1> <h1>Вход в систему</h1>
<?php if ($error): ?> <?php if ($error): ?>
<div class="alert alert-error"> <div class="alert alert-error">
<?= e($error) ?> <?= e($error) ?>
</div> </div>
<?php endif; ?> <?php endif; ?>
<?php if (isset($_SESSION['success'])): ?> <?php if (isset($_SESSION['success'])): ?>
<div class="alert alert-success"> <div class="alert alert-success">
<?= e($_SESSION['success']) ?> <?= e($_SESSION['success']) ?>
<?php unset($_SESSION['success']); ?> <?php unset($_SESSION['success']); ?>
</div> </div>
<?php endif; ?> <?php endif; ?>
<form method="post" style="max-width: 400px; margin: 0 auto;"> <form method="post" style="max-width: 400px; margin: 0 auto;">
<input type="hidden" name="csrf_token" value="<?= generate_csrf_token() ?>"> <input type="hidden" name="csrf_token" value="<?= generate_csrf_token() ?>">
<div style="margin-bottom: 1rem;"> <div style="margin-bottom: 1rem;">
<label for="username" style="display: block; margin-bottom: 0.5rem; font-weight: bold;"> <label for="username" style="display: block; margin-bottom: 0.5rem; font-weight: bold;">
Имя пользователя Имя пользователя
</label> </label>
<input type="text" id="username" name="username" <input type="text" id="username" name="username"
value="<?= e($_POST['username'] ?? '') ?>" value="<?= e($_POST['username'] ?? '') ?>"
placeholder="Введите имя пользователя" placeholder="Введите имя пользователя"
style="width: 100%;" style="width: 100%;"
required> required>
</div> </div>
<div style="margin-bottom: 1.5rem;"> <div style="margin-bottom: 1.5rem;">
<label for="password" style="display: block; margin-bottom: 0.5rem; font-weight: bold;"> <label for="password" style="display: block; margin-bottom: 0.5rem; font-weight: bold;">
Пароль Пароль
</label> </label>
<input type="password" id="password" name="password" <input type="password" id="password" name="password"
placeholder="Введите пароль" placeholder="Введите пароль"
style="width: 100%;" style="width: 100%;"
required> required>
</div> </div>
<button type="submit" class="contrast" style="width: 100%;"> <button type="submit" class="contrast" style="width: 100%;">
🔑 Войти 🔑 Войти
</button> </button>
</form> </form>
<div style="text-align: center; margin-top: 1rem;"> <div style="text-align: center; margin-top: 1rem;">
<p>Нет аккаунта? <a href="register.php">Зарегистрируйтесь здесь</a></p> <p>Нет аккаунта? <a href="register.php">Зарегистрируйтесь здесь</a></p>
</div> </div>
</div> </div>
<?php include 'views/footer.php'; ?> <?php include 'views/footer.php'; ?>

View File

@ -1,204 +1,499 @@
<?php <?php
// models/Book.php // models/Book.php
require_once __DIR__ . '/../includes/parsedown/ParsedownExtra.php';
class Book { class Book {
private $pdo; private $pdo;
public function __construct($pdo) { public function __construct($pdo) {
$this->pdo = $pdo; $this->pdo = $pdo;
} }
public function findById($id) { public function findById($id) {
$stmt = $this->pdo->prepare("SELECT * FROM books WHERE id = ?"); $stmt = $this->pdo->prepare("SELECT * FROM books WHERE id = ?");
$stmt->execute([$id]); $stmt->execute([$id]);
return $stmt->fetch(PDO::FETCH_ASSOC); return $stmt->fetch(PDO::FETCH_ASSOC);
} }
public function findByShareToken($share_token) { public function findByShareToken($share_token) {
$stmt = $this->pdo->prepare("SELECT * FROM books WHERE share_token = ?"); $stmt = $this->pdo->prepare("SELECT * FROM books WHERE share_token = ?");
$stmt->execute([$share_token]); $stmt->execute([$share_token]);
return $stmt->fetch(PDO::FETCH_ASSOC); return $stmt->fetch(PDO::FETCH_ASSOC);
} }
public function findByUser($user_id, $only_published = false) { public function findByUser($user_id, $only_published = false) {
$sql = " $sql = "
SELECT b.*, SELECT b.*,
COUNT(c.id) as chapter_count, COUNT(c.id) as chapter_count,
COALESCE(SUM(c.word_count), 0) as total_words COALESCE(SUM(c.word_count), 0) as total_words
FROM books b FROM books b
LEFT JOIN chapters c ON b.id = c.book_id LEFT JOIN chapters c ON b.id = c.book_id
WHERE b.user_id = ? WHERE b.user_id = ?
"; ";
if ($only_published) { if ($only_published) {
$sql .= " AND b.published = 1 "; $sql .= " AND b.published = 1 ";
} }
$sql .= " GROUP BY b.id ORDER BY b.created_at DESC "; $sql .= " GROUP BY b.id ORDER BY b.created_at DESC ";
$stmt = $this->pdo->prepare($sql); $stmt = $this->pdo->prepare($sql);
$stmt->execute([$user_id]); $stmt->execute([$user_id]);
return $stmt->fetchAll(PDO::FETCH_ASSOC); return $stmt->fetchAll(PDO::FETCH_ASSOC);
} }
public function create($data) { public function create($data) {
$share_token = bin2hex(random_bytes(16)); $share_token = bin2hex(random_bytes(16));
$published = isset($data['published']) ? (int)$data['published'] : 0; $published = isset($data['published']) ? (int)$data['published'] : 0;
$editor_type = $data['editor_type'] ?? 'markdown';
$stmt = $this->pdo->prepare("
INSERT INTO books (title, description, genre, user_id, series_id, sort_order_in_series, share_token, published) $stmt = $this->pdo->prepare("
VALUES (?, ?, ?, ?, ?, ?, ?, ?) INSERT INTO books (title, description, genre, user_id, series_id, sort_order_in_series, share_token, published, editor_type)
"); VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
return $stmt->execute([ ");
$data['title'], return $stmt->execute([
$data['description'] ?? null, $data['title'],
$data['genre'] ?? null, $data['description'] ?? null,
$data['user_id'], $data['genre'] ?? null,
$data['series_id'] ?? null, $data['user_id'],
$data['sort_order_in_series'] ?? null, $data['series_id'] ?? null,
$share_token, $data['sort_order_in_series'] ?? null,
$published $share_token,
]); $published,
} $editor_type
]);
public function update($id, $data) { }
$published = isset($data['published']) ? (int)$data['published'] : 0;
public function update($id, $data) {
$stmt = $this->pdo->prepare(" $published = isset($data['published']) ? (int)$data['published'] : 0;
UPDATE books $editor_type = $data['editor_type'] ?? 'markdown';
SET title = ?, description = ?, genre = ?, series_id = ?, sort_order_in_series = ?, published = ?
WHERE id = ? AND user_id = ? $stmt = $this->pdo->prepare("
"); UPDATE books
return $stmt->execute([ SET title = ?, description = ?, genre = ?, series_id = ?, sort_order_in_series = ?, published = ?, editor_type = ?
$data['title'], WHERE id = ? AND user_id = ?
$data['description'] ?? null, ");
$data['genre'] ?? null, return $stmt->execute([
$data['series_id'] ?? null, $data['title'],
$data['sort_order_in_series'] ?? null, $data['description'] ?? null,
$published, $data['genre'] ?? null,
$id, $data['series_id'] ?? null,
$data['user_id'] $data['sort_order_in_series'] ?? null,
]); $published,
} $editor_type,
$id,
public function delete($id, $user_id) { $data['user_id']
try { ]);
$this->pdo->beginTransaction(); }
// Удаляем главы книги
$stmt = $this->pdo->prepare("DELETE FROM chapters WHERE book_id = ?"); public function delete($id, $user_id) {
$stmt->execute([$id]); try {
$this->pdo->beginTransaction();
// Удаляем саму книгу
$stmt = $this->pdo->prepare("DELETE FROM books WHERE id = ? AND user_id = ?"); // Удаляем главы книги
$result = $stmt->execute([$id, $user_id]); $stmt = $this->pdo->prepare("DELETE FROM chapters WHERE book_id = ?");
$stmt->execute([$id]);
$this->pdo->commit();
return $result; // Удаляем саму книгу
} catch (Exception $e) { $stmt = $this->pdo->prepare("DELETE FROM books WHERE id = ? AND user_id = ?");
$this->pdo->rollBack(); $result = $stmt->execute([$id, $user_id]);
return false;
} $this->pdo->commit();
} return $result;
} catch (Exception $e) {
public function userOwnsBook($book_id, $user_id) { $this->pdo->rollBack();
$stmt = $this->pdo->prepare("SELECT id FROM books WHERE id = ? AND user_id = ?"); return false;
$stmt->execute([$book_id, $user_id]); }
return $stmt->fetch() !== false; }
}
public function userOwnsBook($book_id, $user_id) {
public function generateNewShareToken($book_id) { $stmt = $this->pdo->prepare("SELECT id FROM books WHERE id = ? AND user_id = ?");
$new_token = bin2hex(random_bytes(16)); $stmt->execute([$book_id, $user_id]);
$stmt = $this->pdo->prepare("UPDATE books SET share_token = ? WHERE id = ?"); return $stmt->fetch() !== false;
$success = $stmt->execute([$new_token, $book_id]); }
return $success ? $new_token : false;
} public function generateNewShareToken($book_id) {
$new_token = bin2hex(random_bytes(16));
public function getPublishedChapters($book_id) { $stmt = $this->pdo->prepare("UPDATE books SET share_token = ? WHERE id = ?");
$stmt = $this->pdo->prepare(" $success = $stmt->execute([$new_token, $book_id]);
SELECT * FROM chapters return $success ? $new_token : false;
WHERE book_id = ? AND status = 'published' }
ORDER BY sort_order, created_at
"); public function getPublishedChapters($book_id) {
$stmt->execute([$book_id]); $stmt = $this->pdo->prepare("
return $stmt->fetchAll(PDO::FETCH_ASSOC); SELECT * FROM chapters
} WHERE book_id = ? AND status = 'published'
ORDER BY sort_order, created_at
public function updateCover($book_id, $filename) { ");
$stmt = $this->pdo->prepare("UPDATE books SET cover_image = ? WHERE id = ?"); $stmt->execute([$book_id]);
return $stmt->execute([$filename, $book_id]); return $stmt->fetchAll(PDO::FETCH_ASSOC);
} }
public function deleteCover($book_id) { public function updateCover($book_id, $filename) {
$stmt = $this->pdo->prepare("UPDATE books SET cover_image = ? WHERE id = ?");
$book = $this->findById($book_id); return $stmt->execute([$filename, $book_id]);
$old_filename = $book['cover_image']; }
if ($old_filename) { public function deleteCover($book_id) {
$file_path = COVERS_PATH . $old_filename;
if (file_exists($file_path)) { $book = $this->findById($book_id);
unlink($file_path); $old_filename = $book['cover_image'];
}
} if ($old_filename) {
$file_path = COVERS_PATH . $old_filename;
$stmt = $this->pdo->prepare("UPDATE books SET cover_image = NULL WHERE id = ?"); if (file_exists($file_path)) {
return $stmt->execute([$book_id]); unlink($file_path);
} }
}
public function updateSeriesInfo($book_id, $series_id, $sort_order) {
$stmt = $this->pdo->prepare("UPDATE books SET series_id = ?, sort_order_in_series = ? WHERE id = ?"); $stmt = $this->pdo->prepare("UPDATE books SET cover_image = NULL WHERE id = ?");
return $stmt->execute([$series_id, $sort_order, $book_id]); return $stmt->execute([$book_id]);
} }
public function removeFromSeries($book_id) { public function updateSeriesInfo($book_id, $series_id, $sort_order) {
$stmt = $this->pdo->prepare("UPDATE books SET series_id = NULL, sort_order_in_series = NULL WHERE id = ?"); $stmt = $this->pdo->prepare("UPDATE books SET series_id = ?, sort_order_in_series = ? WHERE id = ?");
return $stmt->execute([$book_id]); return $stmt->execute([$series_id, $sort_order, $book_id]);
} }
public function findBySeries($series_id) { public function removeFromSeries($book_id) {
$stmt = $this->pdo->prepare(" $stmt = $this->pdo->prepare("UPDATE books SET series_id = NULL, sort_order_in_series = NULL WHERE id = ?");
SELECT b.* return $stmt->execute([$book_id]);
FROM books b }
WHERE b.series_id = ?
ORDER BY b.sort_order_in_series, b.created_at public function findBySeries($series_id) {
"); $stmt = $this->pdo->prepare("
$stmt->execute([$series_id]); SELECT b.*
return $stmt->fetchAll(PDO::FETCH_ASSOC); FROM books b
} WHERE b.series_id = ?
ORDER BY b.sort_order_in_series, b.created_at
public function reorderSeriesBooks($series_id, $new_order) { ");
try { $stmt->execute([$series_id]);
$this->pdo->beginTransaction(); return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
foreach ($new_order as $order => $book_id) {
$stmt = $this->pdo->prepare("UPDATE books SET sort_order_in_series = ? WHERE id = ? AND series_id = ?"); public function reorderSeriesBooks($series_id, $new_order) {
$stmt->execute([$order + 1, $book_id, $series_id]); try {
} $this->pdo->beginTransaction();
$this->pdo->commit(); foreach ($new_order as $order => $book_id) {
return true; $stmt = $this->pdo->prepare("UPDATE books SET sort_order_in_series = ? WHERE id = ? AND series_id = ?");
} catch (Exception $e) { $stmt->execute([$order + 1, $book_id, $series_id]);
$this->pdo->rollBack(); }
return false;
} $this->pdo->commit();
} return true;
} catch (Exception $e) {
$this->pdo->rollBack();
public function getBookStats($book_id, $only_published_chapters = false) { return false;
$sql = " }
SELECT }
COUNT(c.id) as chapter_count,
COALESCE(SUM(c.word_count), 0) as total_words
FROM books b public function getBookStats($book_id, $only_published_chapters = false) {
LEFT JOIN chapters c ON b.id = c.book_id $sql = "
WHERE b.id = ? SELECT
"; COUNT(c.id) as chapter_count,
COALESCE(SUM(c.word_count), 0) as total_words
if ($only_published_chapters) { FROM books b
$sql .= " AND c.status = 'published'"; LEFT JOIN chapters c ON b.id = c.book_id
} WHERE b.id = ?
";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([$book_id]); if ($only_published_chapters) {
return $stmt->fetch(PDO::FETCH_ASSOC); $sql .= " AND c.status = 'published'";
} }
} $stmt = $this->pdo->prepare($sql);
$stmt->execute([$book_id]);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
public function convertChaptersContent($book_id, $from_editor, $to_editor) {
try {
$this->pdo->beginTransaction();
$chapters = $this->getAllChapters($book_id);
foreach ($chapters as $chapter) {
$converted_content = $this->convertContent(
$chapter['content'],
$from_editor,
$to_editor
);
$this->updateChapterContent($chapter['id'], $converted_content);
}
$this->pdo->commit();
return true;
} catch (Exception $e) {
$this->pdo->rollBack();
error_log("Error converting chapters: " . $e->getMessage());
return false;
}
}
private function getAllChapters($book_id) {
$stmt = $this->pdo->prepare("SELECT id, content FROM chapters WHERE book_id = ?");
$stmt->execute([$book_id]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
private function updateChapterContent($chapter_id, $content) {
$word_count = $this->countWords($content);
$stmt = $this->pdo->prepare("
UPDATE chapters
SET content = ?, word_count = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
");
return $stmt->execute([$content, $word_count, $chapter_id]);
}
private function countWords($text) {
$text = strip_tags($text);
$text = preg_replace('/[^\p{L}\p{N}\s]/u', ' ', $text);
$words = preg_split('/\s+/', $text);
$words = array_filter($words);
return count($words);
}
private function convertContent($content, $from_editor, $to_editor) {
if ($from_editor === $to_editor) {
return $content;
}
try {
if ($from_editor === 'markdown' && $to_editor === 'html') {
// Markdown to HTML с улучшенной обработкой абзацев
return $this->markdownToHtmlWithParagraphs($content);
} elseif ($from_editor === 'html' && $to_editor === 'markdown') {
// HTML to Markdown
return $this->htmlToMarkdown($content);
}
} catch (Exception $e) {
error_log("Error converting content from {$from_editor} to {$to_editor}: " . $e->getMessage());
return $content;
}
return $content;
}
private function markdownToHtmlWithParagraphs($markdown) {
$parsedown = new ParsedownExtra();
// Включаем разметку строк для лучшей обработки абзацев
$parsedown->setBreaksEnabled(true);
// Обрабатываем Markdown
$html = $parsedown->text($markdown);
// Дополнительная обработка для обеспечения правильной структуры абзацев
$html = $this->ensureParagraphStructure($html);
return $html;
}
private function ensureParagraphStructure($html) {
// Если HTML не содержит тегов абзацев или div'ов, оборачиваем в <p>
if (!preg_match('/<(p|div|h[1-6]|blockquote|pre|ul|ol|li)/i', $html)) {
// Разбиваем на строки и оборачиваем каждую непустую строку в <p>
$lines = explode("\n", trim($html));
$wrappedLines = [];
foreach ($lines as $line) {
$line = trim($line);
if (!empty($line)) {
// Пропускаем уже обернутые строки
if (!preg_match('/^<[^>]+>/', $line) || preg_match('/^<(p|div|h[1-6])/i', $line)) {
$wrappedLines[] = $line;
} else {
$wrappedLines[] = "<p>{$line}</p>";
}
}
}
$html = implode("\n", $wrappedLines);
}
// Убеждаемся, что теги правильно закрыты
$html = $this->balanceTags($html);
return $html;
}
private function balanceTags($html) {
// Простая балансировка тегов - в реальном проекте лучше использовать DOMDocument
$tags = [
'p' => 0,
'div' => 0,
'span' => 0,
'strong' => 0,
'em' => 0,
];
// Счетчик открывающих и закрывающих тегов
foreach ($tags as $tag => &$count) {
$open = substr_count($html, "<{$tag}>") + substr_count($html, "<{$tag} ");
$close = substr_count($html, "</{$tag}>");
$count = $open - $close;
}
// Добавляем недостающие закрывающие теги
foreach ($tags as $tag => $count) {
if ($count > 0) {
$html .= str_repeat("</{$tag}>", $count);
}
}
return $html;
}
private function htmlToMarkdown($html) {
// Сначала нормализуем HTML структуру
$html = $this->normalizeHtml($html);
// Базовая конвертация HTML в Markdown
$markdown = $html;
// Обрабатываем абзацы - заменяем на двойные переносы строк
$markdown = preg_replace('/<p[^>]*>(.*?)<\/p>/is', "$1\n\n", $markdown);
// Обрабатываем разрывы строк
$markdown = preg_replace('/<br[^>]*>\s*<\/br[^>]*>/i', "\n", $markdown);
$markdown = preg_replace('/<br[^>]*>/i', " \n", $markdown); // Два пробела для Markdown разрыва
// Заголовки
$markdown = preg_replace('/<h1[^>]*>(.*?)<\/h1>/is', "# $1\n\n", $markdown);
$markdown = preg_replace('/<h2[^>]*>(.*?)<\/h2>/is', "## $1\n\n", $markdown);
$markdown = preg_replace('/<h3[^>]*>(.*?)<\/h3>/is', "### $1\n\n", $markdown);
$markdown = preg_replace('/<h4[^>]*>(.*?)<\/h4>/is', "#### $1\n\n", $markdown);
$markdown = preg_replace('/<h5[^>]*>(.*?)<\/h5>/is', "##### $1\n\n", $markdown);
$markdown = preg_replace('/<h6[^>]*>(.*?)<\/h6>/is', "###### $1\n\n", $markdown);
// Жирный текст
$markdown = preg_replace('/<strong[^>]*>(.*?)<\/strong>/is', '**$1**', $markdown);
$markdown = preg_replace('/<b[^>]*>(.*?)<\/b>/is', '**$1**', $markdown);
// Курсив
$markdown = preg_replace('/<em[^>]*>(.*?)<\/em>/is', '*$1*', $markdown);
$markdown = preg_replace('/<i[^>]*>(.*?)<\/i>/is', '*$1*', $markdown);
// Подчеркивание (не стандартно в Markdown, но обрабатываем)
$markdown = preg_replace('/<u[^>]*>(.*?)<\/u>/is', '<u>$1</u>', $markdown);
// Зачеркивание
$markdown = preg_replace('/<s[^>]*>(.*?)<\/s>/is', '~~$1~~', $markdown);
$markdown = preg_replace('/<strike[^>]*>(.*?)<\/strike>/is', '~~$1~~', $markdown);
$markdown = preg_replace('/<del[^>]*>(.*?)<\/del>/is', '~~$1~~', $markdown);
// Списки
$markdown = preg_replace('/<li[^>]*>(.*?)<\/li>/is', '- $1', $markdown);
$markdown = preg_replace('/<ul[^>]*>(.*?)<\/ul>/is', "$1\n", $markdown);
$markdown = preg_replace('/<ol[^>]*>(.*?)<\/ol>/is', "$1\n", $markdown);
// Блочные цитаты
$markdown = preg_replace('/<blockquote[^>]*>(.*?)<\/blockquote>/is', "> $1\n", $markdown);
// Код
$markdown = preg_replace('/<code[^>]*>(.*?)<\/code>/is', '`$1`', $markdown);
$markdown = preg_replace('/<pre[^>]*>(.*?)<\/pre>/is', "```\n$1\n```", $markdown);
// Ссылки
$markdown = preg_replace('/<a[^>]*href="([^"]*)"[^>]*>(.*?)<\/a>/is', '[$2]($1)', $markdown);
// Изображения
$markdown = preg_replace('/<img[^>]*src="([^"]*)"[^>]*alt="([^"]*)"[^>]*>/is', '![$2]($1)', $markdown);
// Удаляем все остальные HTML-теги
$markdown = strip_tags($markdown);
// Чистим лишние пробелы и переносы
$markdown = preg_replace('/\n\s*\n\s*\n/', "\n\n", $markdown);
$markdown = preg_replace('/^\s+|\s+$/m', '', $markdown); // Trim каждой строки
$markdown = trim($markdown);
return $markdown;
}
private function normalizeHtml($html) {
// Нормализуем HTML структуру перед конвертацией
$html = preg_replace('/<div[^>]*>(.*?)<\/div>/is', "<p>$1</p>", $html);
// Убираем лишние пробелы
$html = preg_replace('/\s+/', ' ', $html);
// Восстанавливаем структуру абзацев
$html = preg_replace('/([^>])\s*<\/(p|div)>\s*([^<])/', "$1</$2>\n\n$3", $html);
return $html;
}
public function normalizeBookContent($book_id) {
try {
$chapters = $this->getAllChapters($book_id);
$book = $this->findById($book_id);
foreach ($chapters as $chapter) {
$normalized_content = '';
if ($book['editor_type'] == 'html') {
// Нормализуем HTML контент
$normalized_content = $this->normalizeHtmlContent($chapter['content']);
} else {
// Нормализуем Markdown контент
$normalized_content = $this->normalizeMarkdownContent($chapter['content']);
}
if ($normalized_content !== $chapter['content']) {
$this->updateChapterContent($chapter['id'], $normalized_content);
}
}
return true;
} catch (Exception $e) {
error_log("Error normalizing book content: " . $e->getMessage());
return false;
}
}
private function normalizeHtmlContent($html) {
// Простая нормализация HTML - оборачиваем текст без тегов в <p>
if (!preg_match('/<[^>]+>/', $html) && trim($html) !== '') {
// Если нет HTML тегов, оборачиваем в <p>
$lines = explode("\n", trim($html));
$wrapped = array_map(function($line) {
$line = trim($line);
return $line ? "<p>{$line}</p>" : '';
}, $lines);
return implode("\n", array_filter($wrapped));
}
return $html;
}
private function normalizeMarkdownContent($markdown) {
// Нормализация Markdown - убеждаемся, что есть пустые строки между абзацами
$lines = explode("\n", $markdown);
$normalized = [];
$inParagraph = false;
foreach ($lines as $line) {
$trimmed = trim($line);
if (empty($trimmed)) {
// Пустая строка - конец абзаца
if ($inParagraph) {
$normalized[] = '';
$inParagraph = false;
}
} else {
// Непустая строка
if (!$inParagraph && !empty($normalized) && end($normalized) !== '') {
// Добавляем пустую строку перед новым абзацем
$normalized[] = '';
}
$normalized[] = $line;
$inParagraph = true;
}
}
return implode("\n", $normalized);
}
}
?> ?>

314
models/Series.php Executable file → Normal file
View File

@ -1,158 +1,158 @@
<?php <?php
// models/Series.php // models/Series.php
class Series { class Series {
private $pdo; private $pdo;
public function __construct($pdo) { public function __construct($pdo) {
$this->pdo = $pdo; $this->pdo = $pdo;
} }
public function findById($id) { public function findById($id) {
$stmt = $this->pdo->prepare(" $stmt = $this->pdo->prepare("
SELECT s.*, SELECT s.*,
COUNT(b.id) as book_count, COUNT(b.id) as book_count,
COALESCE(( COALESCE((
SELECT SUM(c.word_count) SELECT SUM(c.word_count)
FROM chapters c FROM chapters c
JOIN books b2 ON c.book_id = b2.id JOIN books b2 ON c.book_id = b2.id
WHERE b2.series_id = s.id AND b2.published = 1 WHERE b2.series_id = s.id AND b2.published = 1
), 0) as total_words ), 0) as total_words
FROM series s FROM series s
LEFT JOIN books b ON s.id = b.series_id AND b.published = 1 LEFT JOIN books b ON s.id = b.series_id AND b.published = 1
WHERE s.id = ? WHERE s.id = ?
GROUP BY s.id GROUP BY s.id
"); ");
$stmt->execute([$id]); $stmt->execute([$id]);
return $stmt->fetch(PDO::FETCH_ASSOC); return $stmt->fetch(PDO::FETCH_ASSOC);
} }
public function findByUser($user_id, $include_stats = true) { public function findByUser($user_id, $include_stats = true) {
if ($include_stats) { if ($include_stats) {
$sql = " $sql = "
SELECT s.*, SELECT s.*,
COUNT(b.id) as book_count, COUNT(b.id) as book_count,
COALESCE(( COALESCE((
SELECT SUM(c.word_count) SELECT SUM(c.word_count)
FROM chapters c FROM chapters c
JOIN books b2 ON c.book_id = b2.id JOIN books b2 ON c.book_id = b2.id
WHERE b2.series_id = s.id AND b2.user_id = ? WHERE b2.series_id = s.id AND b2.user_id = ?
), 0) as total_words ), 0) as total_words
FROM series s FROM series s
LEFT JOIN books b ON s.id = b.series_id LEFT JOIN books b ON s.id = b.series_id
WHERE s.user_id = ? WHERE s.user_id = ?
GROUP BY s.id GROUP BY s.id
ORDER BY s.created_at DESC ORDER BY s.created_at DESC
"; ";
$stmt = $this->pdo->prepare($sql); $stmt = $this->pdo->prepare($sql);
$stmt->execute([$user_id, $user_id]); $stmt->execute([$user_id, $user_id]);
} else { } else {
$sql = "SELECT * FROM series WHERE user_id = ? ORDER BY created_at DESC"; $sql = "SELECT * FROM series WHERE user_id = ? ORDER BY created_at DESC";
$stmt = $this->pdo->prepare($sql); $stmt = $this->pdo->prepare($sql);
$stmt->execute([$user_id]); $stmt->execute([$user_id]);
} }
return $stmt->fetchAll(PDO::FETCH_ASSOC); return $stmt->fetchAll(PDO::FETCH_ASSOC);
} }
public function create($data) { public function create($data) {
$stmt = $this->pdo->prepare(" $stmt = $this->pdo->prepare("
INSERT INTO series (title, description, user_id) INSERT INTO series (title, description, user_id)
VALUES (?, ?, ?) VALUES (?, ?, ?)
"); ");
return $stmt->execute([ return $stmt->execute([
$data['title'], $data['title'],
$data['description'] ?? null, $data['description'] ?? null,
$data['user_id'] $data['user_id']
]); ]);
} }
public function update($id, $data) { public function update($id, $data) {
$stmt = $this->pdo->prepare(" $stmt = $this->pdo->prepare("
UPDATE series UPDATE series
SET title = ?, description = ?, updated_at = CURRENT_TIMESTAMP SET title = ?, description = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ? AND user_id = ? WHERE id = ? AND user_id = ?
"); ");
return $stmt->execute([ return $stmt->execute([
$data['title'], $data['title'],
$data['description'] ?? null, $data['description'] ?? null,
$id, $id,
$data['user_id'] $data['user_id']
]); ]);
} }
public function delete($id, $user_id) { public function delete($id, $user_id) {
try { try {
$this->pdo->beginTransaction(); $this->pdo->beginTransaction();
$stmt = $this->pdo->prepare("UPDATE books SET series_id = NULL, sort_order_in_series = NULL WHERE series_id = ? AND user_id = ?"); $stmt = $this->pdo->prepare("UPDATE books SET series_id = NULL, sort_order_in_series = NULL WHERE series_id = ? AND user_id = ?");
$stmt->execute([$id, $user_id]); $stmt->execute([$id, $user_id]);
$stmt = $this->pdo->prepare("DELETE FROM series WHERE id = ? AND user_id = ?"); $stmt = $this->pdo->prepare("DELETE FROM series WHERE id = ? AND user_id = ?");
$result = $stmt->execute([$id, $user_id]); $result = $stmt->execute([$id, $user_id]);
$this->pdo->commit(); $this->pdo->commit();
return $result; return $result;
} catch (Exception $e) { } catch (Exception $e) {
$this->pdo->rollBack(); $this->pdo->rollBack();
return false; return false;
} }
} }
public function userOwnsSeries($series_id, $user_id) { public function userOwnsSeries($series_id, $user_id) {
$stmt = $this->pdo->prepare("SELECT id FROM series WHERE id = ? AND user_id = ?"); $stmt = $this->pdo->prepare("SELECT id FROM series WHERE id = ? AND user_id = ?");
$stmt->execute([$series_id, $user_id]); $stmt->execute([$series_id, $user_id]);
return $stmt->fetch() !== false; return $stmt->fetch() !== false;
} }
public function getBooksInSeries($series_id, $only_published = false) { public function getBooksInSeries($series_id, $only_published = false) {
$sql = "SELECT * FROM books WHERE series_id = ?"; $sql = "SELECT * FROM books WHERE series_id = ?";
if ($only_published) { if ($only_published) {
$sql .= " AND published = 1"; $sql .= " AND published = 1";
} }
$sql .= " ORDER BY sort_order_in_series, created_at"; $sql .= " ORDER BY sort_order_in_series, created_at";
$stmt = $this->pdo->prepare($sql); $stmt = $this->pdo->prepare($sql);
$stmt->execute([$series_id]); $stmt->execute([$series_id]);
return $stmt->fetchAll(PDO::FETCH_ASSOC); return $stmt->fetchAll(PDO::FETCH_ASSOC);
} }
public function getNextSortOrder($series_id) { public function getNextSortOrder($series_id) {
$stmt = $this->pdo->prepare("SELECT MAX(sort_order_in_series) as max_order FROM books WHERE series_id = ?"); $stmt = $this->pdo->prepare("SELECT MAX(sort_order_in_series) as max_order FROM books WHERE series_id = ?");
$stmt->execute([$series_id]); $stmt->execute([$series_id]);
$result = $stmt->fetch(); $result = $stmt->fetch();
return ($result['max_order'] ?? 0) + 1; return ($result['max_order'] ?? 0) + 1;
} }
public function getSeriesStats($series_id, $user_id = null) { public function getSeriesStats($series_id, $user_id = null) {
$sql = " $sql = "
SELECT SELECT
COUNT(b.id) as book_count, COUNT(b.id) as book_count,
COALESCE(SUM(stats.chapter_count), 0) as chapter_count, COALESCE(SUM(stats.chapter_count), 0) as chapter_count,
COALESCE(SUM(stats.total_words), 0) as total_words COALESCE(SUM(stats.total_words), 0) as total_words
FROM series s FROM series s
LEFT JOIN books b ON s.id = b.series_id LEFT JOIN books b ON s.id = b.series_id
LEFT JOIN ( LEFT JOIN (
SELECT SELECT
book_id, book_id,
COUNT(id) as chapter_count, COUNT(id) as chapter_count,
SUM(word_count) as total_words SUM(word_count) as total_words
FROM chapters FROM chapters
GROUP BY book_id GROUP BY book_id
) stats ON b.id = stats.book_id ) stats ON b.id = stats.book_id
WHERE s.id = ? WHERE s.id = ?
"; ";
$params = [$series_id]; $params = [$series_id];
if ($user_id) { if ($user_id) {
$sql .= " AND s.user_id = ?"; $sql .= " AND s.user_id = ?";
$params[] = $user_id; $params[] = $user_id;
} }
$stmt = $this->pdo->prepare($sql); $stmt = $this->pdo->prepare($sql);
$stmt->execute($params); $stmt->execute($params);
return $stmt->fetch(PDO::FETCH_ASSOC); return $stmt->fetch(PDO::FETCH_ASSOC);
} }
} }
?> ?>

View File

@ -1,119 +1,119 @@
<?php <?php
// models/User.php // models/User.php
class User { class User {
private $pdo; private $pdo;
public function __construct($pdo) { public function __construct($pdo) {
$this->pdo = $pdo; $this->pdo = $pdo;
} }
public function findById($id) { public function findById($id) {
$stmt = $this->pdo->prepare("SELECT * FROM users WHERE id = ?"); $stmt = $this->pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$id]); $stmt->execute([$id]);
return $stmt->fetch(PDO::FETCH_ASSOC); return $stmt->fetch(PDO::FETCH_ASSOC);
} }
public function findByUsername($username) { public function findByUsername($username) {
$stmt = $this->pdo->prepare("SELECT * FROM users WHERE username = ?"); $stmt = $this->pdo->prepare("SELECT * FROM users WHERE username = ?");
$stmt->execute([$username]); $stmt->execute([$username]);
return $stmt->fetch(PDO::FETCH_ASSOC); return $stmt->fetch(PDO::FETCH_ASSOC);
} }
public function findByEmail($email) { public function findByEmail($email) {
$stmt = $this->pdo->prepare("SELECT * FROM users WHERE email = ?"); $stmt = $this->pdo->prepare("SELECT * FROM users WHERE email = ?");
$stmt->execute([$email]); $stmt->execute([$email]);
return $stmt->fetch(PDO::FETCH_ASSOC); return $stmt->fetch(PDO::FETCH_ASSOC);
} }
public function findAll() { public function findAll() {
$stmt = $this->pdo->prepare("SELECT id, username, display_name, email, created_at, last_login, is_active FROM users ORDER BY created_at DESC"); $stmt = $this->pdo->prepare("SELECT id, username, display_name, email, created_at, last_login, is_active FROM users ORDER BY created_at DESC");
$stmt->execute(); $stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC); return $stmt->fetchAll(PDO::FETCH_ASSOC);
} }
public function create($data) { public function create($data) {
$password_hash = password_hash($data['password'], PASSWORD_DEFAULT); $password_hash = password_hash($data['password'], PASSWORD_DEFAULT);
$is_active = $data['is_active'] ?? 0; $is_active = $data['is_active'] ?? 0;
$stmt = $this->pdo->prepare(" $stmt = $this->pdo->prepare("
INSERT INTO users (username, display_name, email, password_hash, is_active) INSERT INTO users (username, display_name, email, password_hash, is_active)
VALUES (?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?)
"); ");
return $stmt->execute([ return $stmt->execute([
$data['username'], $data['username'],
$data['display_name'] ?? $data['username'], $data['display_name'] ?? $data['username'],
$data['email'] ?? null, $data['email'] ?? null,
$password_hash, $password_hash,
$is_active $is_active
]); ]);
} }
public function update($id, $data) { public function update($id, $data) {
$sql = "UPDATE users SET display_name = ?, email = ?"; $sql = "UPDATE users SET display_name = ?, email = ?";
$params = [$data['display_name'], $data['email']]; $params = [$data['display_name'], $data['email']];
if (!empty($data['password'])) { if (!empty($data['password'])) {
$sql .= ", password_hash = ?"; $sql .= ", password_hash = ?";
$params[] = password_hash($data['password'], PASSWORD_DEFAULT); $params[] = password_hash($data['password'], PASSWORD_DEFAULT);
} }
$sql .= " WHERE id = ?"; $sql .= " WHERE id = ?";
$params[] = $id; $params[] = $id;
$stmt = $this->pdo->prepare($sql); $stmt = $this->pdo->prepare($sql);
return $stmt->execute($params); return $stmt->execute($params);
} }
public function updateStatus($id, $is_active) { public function updateStatus($id, $is_active) {
$stmt = $this->pdo->prepare("UPDATE users SET is_active = ? WHERE id = ?"); $stmt = $this->pdo->prepare("UPDATE users SET is_active = ? WHERE id = ?");
return $stmt->execute([$is_active, $id]); return $stmt->execute([$is_active, $id]);
} }
public function delete($id) { public function delete($id) {
$stmt = $this->pdo->prepare("DELETE FROM users WHERE id = ?"); $stmt = $this->pdo->prepare("DELETE FROM users WHERE id = ?");
return $stmt->execute([$id]); return $stmt->execute([$id]);
} }
public function updateLastLogin($id) { public function updateLastLogin($id) {
$stmt = $this->pdo->prepare("UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?"); $stmt = $this->pdo->prepare("UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?");
return $stmt->execute([$id]); return $stmt->execute([$id]);
} }
public function verifyPassword($password, $hash) { public function verifyPassword($password, $hash) {
return password_verify($password, $hash); return password_verify($password, $hash);
} }
public function updateAvatar($id, $filename) { public function updateAvatar($id, $filename) {
$stmt = $this->pdo->prepare("UPDATE users SET avatar = ? WHERE id = ?"); $stmt = $this->pdo->prepare("UPDATE users SET avatar = ? WHERE id = ?");
return $stmt->execute([$filename, $id]); return $stmt->execute([$filename, $id]);
} }
public function updateBio($id, $bio) { public function updateBio($id, $bio) {
$stmt = $this->pdo->prepare("UPDATE users SET bio = ? WHERE id = ?"); $stmt = $this->pdo->prepare("UPDATE users SET bio = ? WHERE id = ?");
return $stmt->execute([$bio, $id]); return $stmt->execute([$bio, $id]);
} }
public function updateProfile($id, $data) { public function updateProfile($id, $data) {
$sql = "UPDATE users SET display_name = ?, email = ?, bio = ?"; $sql = "UPDATE users SET display_name = ?, email = ?, bio = ?";
$params = [ $params = [
$data['display_name'] ?? '', $data['display_name'] ?? '',
$data['email'] ?? null, $data['email'] ?? null,
$data['bio'] ?? null $data['bio'] ?? null
]; ];
if (!empty($data['avatar'])) { if (!empty($data['avatar'])) {
$sql .= ", avatar = ?"; $sql .= ", avatar = ?";
$params[] = $data['avatar']; $params[] = $data['avatar'];
} }
$sql .= " WHERE id = ?"; $sql .= " WHERE id = ?";
$params[] = $id; $params[] = $id;
$stmt = $this->pdo->prepare($sql); $stmt = $this->pdo->prepare($sql);
return $stmt->execute($params); return $stmt->execute($params);
} }
} }
?> ?>

View File

@ -7,9 +7,15 @@ $Parsedown = new ParsedownExtra();;
$content = $_POST['content'] ?? ''; $content = $_POST['content'] ?? '';
$title = $_POST['title'] ?? 'Предпросмотр'; $title = $_POST['title'] ?? 'Предпросмотр';
$editor_type = $_POST['editor_type'] ?? 'markdown'; // Новое поле
// Обрабатываем контент в зависимости от типа редактора
if ($editor_type == 'markdown') {
$html_content = $Parsedown->text($content);
} else {
$html_content = $content;
}
$Parsedown = new Parsedown();
$html_content = $Parsedown->text($content);
$page_title = "Предпросмотр: " . e($title); $page_title = "Предпросмотр: " . e($title);
?> ?>

View File

@ -1,195 +1,195 @@
<?php <?php
require_once 'config/config.php'; require_once 'config/config.php';
require_login(); require_login();
$user_id = $_SESSION['user_id']; $user_id = $_SESSION['user_id'];
$userModel = new User($pdo); $userModel = new User($pdo);
$user = $userModel->findById($user_id); $user = $userModel->findById($user_id);
$message = ''; $message = '';
$avatar_error = ''; $avatar_error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!verify_csrf_token($_POST['csrf_token'] ?? '')) { if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
$message = "Ошибка безопасности"; $message = "Ошибка безопасности";
} else { } else {
$display_name = trim($_POST['display_name'] ?? ''); $display_name = trim($_POST['display_name'] ?? '');
$email = trim($_POST['email'] ?? ''); $email = trim($_POST['email'] ?? '');
$bio = trim($_POST['bio'] ?? ''); $bio = trim($_POST['bio'] ?? '');
// Обработка загрузки аватарки // Обработка загрузки аватарки
if (isset($_FILES['avatar']) && $_FILES['avatar']['error'] === UPLOAD_ERR_OK) { if (isset($_FILES['avatar']) && $_FILES['avatar']['error'] === UPLOAD_ERR_OK) {
$avatar_result = handleAvatarUpload($_FILES['avatar'], $user_id); $avatar_result = handleAvatarUpload($_FILES['avatar'], $user_id);
if ($avatar_result['success']) { if ($avatar_result['success']) {
$userModel->updateAvatar($user_id, $avatar_result['filename']); $userModel->updateAvatar($user_id, $avatar_result['filename']);
// Обновляем данные пользователя // Обновляем данные пользователя
$user = $userModel->findById($user_id); $user = $userModel->findById($user_id);
} else { } else {
$avatar_error = $avatar_result['error']; $avatar_error = $avatar_result['error'];
} }
} }
// Обработка удаления аватарки // Обработка удаления аватарки
if (isset($_POST['delete_avatar']) && $_POST['delete_avatar'] == '1') { if (isset($_POST['delete_avatar']) && $_POST['delete_avatar'] == '1') {
deleteUserAvatar($user_id); deleteUserAvatar($user_id);
$user = $userModel->findById($user_id); $user = $userModel->findById($user_id);
} }
// Обновляем основные данные // Обновляем основные данные
$data = [ $data = [
'display_name' => $display_name, 'display_name' => $display_name,
'email' => $email, 'email' => $email,
'bio' => $bio 'bio' => $bio
]; ];
if ($userModel->updateProfile($user_id, $data)) { if ($userModel->updateProfile($user_id, $data)) {
$_SESSION['display_name'] = $display_name ?: $user['username']; $_SESSION['display_name'] = $display_name ?: $user['username'];
$message = "Профиль обновлен"; $message = "Профиль обновлен";
// Обновляем данные пользователя // Обновляем данные пользователя
$user = $userModel->findById($user_id); $user = $userModel->findById($user_id);
} else { } else {
$message = "Ошибка при обновлении профиля"; $message = "Ошибка при обновлении профиля";
} }
} }
} }
$page_title = "Мой профиль"; $page_title = "Мой профиль";
include 'views/header.php'; include 'views/header.php';
?> ?>
<h1>Мой профиль</h1> <h1>Мой профиль</h1>
<?php if ($message): ?> <?php if ($message): ?>
<div class="alert <?= strpos($message, 'Ошибка') !== false ? 'alert-error' : 'alert-success' ?>"> <div class="alert <?= strpos($message, 'Ошибка') !== false ? 'alert-error' : 'alert-success' ?>">
<?= e($message) ?> <?= e($message) ?>
</div> </div>
<?php endif; ?> <?php endif; ?>
<div class="grid"> <div class="grid">
<article> <article>
<h2>Основная информация</h2> <h2>Основная информация</h2>
<form method="post" enctype="multipart/form-data"> <form method="post" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="<?= generate_csrf_token() ?>"> <input type="hidden" name="csrf_token" value="<?= generate_csrf_token() ?>">
<div style="margin-bottom: 1rem;"> <div style="margin-bottom: 1rem;">
<label for="username" style="display: block; margin-bottom: 0.5rem; font-weight: bold;"> <label for="username" style="display: block; margin-bottom: 0.5rem; font-weight: bold;">
Имя пользователя (нельзя изменить) Имя пользователя (нельзя изменить)
</label> </label>
<input type="text" id="username" value="<?= e($user['username']) ?>" disabled style="width: 100%;"> <input type="text" id="username" value="<?= e($user['username']) ?>" disabled style="width: 100%;">
</div> </div>
<div style="margin-bottom: 1rem;"> <div style="margin-bottom: 1rem;">
<label for="display_name" style="display: block; margin-bottom: 0.5rem; font-weight: bold;"> <label for="display_name" style="display: block; margin-bottom: 0.5rem; font-weight: bold;">
Отображаемое имя * Отображаемое имя *
</label> </label>
<input type="text" id="display_name" name="display_name" <input type="text" id="display_name" name="display_name"
value="<?= e($user['display_name'] ?? $user['username']) ?>" value="<?= e($user['display_name'] ?? $user['username']) ?>"
style="width: 100%;" required> style="width: 100%;" required>
</div> </div>
<div style="margin-bottom: 1.5rem;"> <div style="margin-bottom: 1.5rem;">
<label for="email" style="display: block; margin-bottom: 0.5rem; font-weight: bold;"> <label for="email" style="display: block; margin-bottom: 0.5rem; font-weight: bold;">
Email Email
</label> </label>
<input type="email" id="email" name="email" <input type="email" id="email" name="email"
value="<?= e($user['email'] ?? '') ?>" value="<?= e($user['email'] ?? '') ?>"
style="width: 100%;"> style="width: 100%;">
</div> </div>
<div style="margin-bottom: 1.5rem;"> <div style="margin-bottom: 1.5rem;">
<label for="bio" style="display: block; margin-bottom: 0.5rem; font-weight: bold;"> <label for="bio" style="display: block; margin-bottom: 0.5rem; font-weight: bold;">
О себе (отображается на вашей публичной странице) О себе (отображается на вашей публичной странице)
</label> </label>
<textarea id="bio" name="bio" <textarea id="bio" name="bio"
placeholder="Расскажите о себе, своих интересах, стиле письма..." placeholder="Расскажите о себе, своих интересах, стиле письма..."
rows="6" rows="6"
style="width: 100%;"><?= e($user['bio'] ?? '') ?></textarea> style="width: 100%;"><?= e($user['bio'] ?? '') ?></textarea>
<small style="color: #666;"> <small style="color: #666;">
Поддерживается Markdown форматирование Поддерживается Markdown форматирование
</small> </small>
</div> </div>
<div class="profile-buttons"> <div class="profile-buttons">
<button type="submit" class="profile-button primary"> <button type="submit" class="profile-button primary">
💾 Сохранить изменения 💾 Сохранить изменения
</button> </button>
<a href="dashboard.php" class="profile-button secondary"> <a href="dashboard.php" class="profile-button secondary">
↩️ Назад ↩️ Назад
</a> </a>
</div> </div>
</form> </form>
</article> </article>
<article> <article>
<h2>Аватарка</h2> <h2>Аватарка</h2>
<div style="text-align: center; margin-bottom: 1.5rem;"> <div style="text-align: center; margin-bottom: 1.5rem;">
<?php if (!empty($user['avatar'])): ?> <?php if (!empty($user['avatar'])): ?>
<img src="<?= AVATARS_URL . e($user['avatar']) ?>" <img src="<?= AVATARS_URL . e($user['avatar']) ?>"
alt="Аватарка" alt="Аватарка"
style="max-width: 200px; height: auto; border-radius: 50%; border: 3px solid #007bff;" style="max-width: 200px; height: auto; border-radius: 50%; border: 3px solid #007bff;"
onerror="this.style.display='none'"> onerror="this.style.display='none'">
<?php else: ?> <?php else: ?>
<div style="width: 200px; height: 200px; border-radius: 50%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; align-items: center; justify-content: center; color: white; font-size: 4rem; margin: 0 auto;"> <div style="width: 200px; height: 200px; border-radius: 50%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; align-items: center; justify-content: center; color: white; font-size: 4rem; margin: 0 auto;">
<?= mb_substr(e($user['display_name'] ?? $user['username']), 0, 1) ?> <?= mb_substr(e($user['display_name'] ?? $user['username']), 0, 1) ?>
</div> </div>
<?php endif; ?> <?php endif; ?>
</div> </div>
<form method="post" enctype="multipart/form-data"> <form method="post" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="<?= generate_csrf_token() ?>"> <input type="hidden" name="csrf_token" value="<?= generate_csrf_token() ?>">
<div style="margin-bottom: 1rem;"> <div style="margin-bottom: 1rem;">
<label for="avatar" style="display: block; margin-bottom: 0.5rem; font-weight: bold;"> <label for="avatar" style="display: block; margin-bottom: 0.5rem; font-weight: bold;">
Загрузить новую аватарку Загрузить новую аватарку
</label> </label>
<input type="file" id="avatar" name="avatar" <input type="file" id="avatar" name="avatar"
accept="image/jpeg, image/png, image/gif, image/webp" accept="image/jpeg, image/png, image/gif, image/webp"
style="height: 2.6rem;"> style="height: 2.6rem;">
<small style="color: #666;"> <small style="color: #666;">
Разрешены: JPG, PNG, GIF, WebP. Максимальный размер: 2MB. Разрешены: JPG, PNG, GIF, WebP. Максимальный размер: 2MB.
Рекомендуемый размер: 200×200 пикселей. Рекомендуемый размер: 200×200 пикселей.
</small> </small>
<?php if (!empty($avatar_error)): ?> <?php if (!empty($avatar_error)): ?>
<div style="color: #d32f2f; margin-top: 0.5rem;"> <div style="color: #d32f2f; margin-top: 0.5rem;">
<?= e($avatar_error) ?> <?= e($avatar_error) ?>
</div> </div>
<?php endif; ?> <?php endif; ?>
</div> </div>
<div style="display: flex; gap: 10px;"> <div style="display: flex; gap: 10px;">
<button type="submit" class="contrast" style="flex: 1;"> <button type="submit" class="contrast" style="flex: 1;">
📤 Загрузить аватарку 📤 Загрузить аватарку
</button> </button>
<?php if (!empty($user['avatar'])): ?> <?php if (!empty($user['avatar'])): ?>
<button type="submit" name="delete_avatar" value="1" class="secondary" style="flex: 1; background: #ff4444; border-color: #ff4444; color: white;"> <button type="submit" name="delete_avatar" value="1" class="secondary" style="flex: 1; background: #ff4444; border-color: #ff4444; color: white;">
🗑️ Удалить аватарку 🗑️ Удалить аватарку
</button> </button>
<?php endif; ?> <?php endif; ?>
</div> </div>
</form> </form>
<?php if (!empty($user['avatar'])): ?> <?php if (!empty($user['avatar'])): ?>
<div style="margin-top: 1rem; padding: 1rem; background: #f8f9fa; border-radius: 5px;"> <div style="margin-top: 1rem; padding: 1rem; background: #f8f9fa; border-radius: 5px;">
<p style="margin: 0; font-size: 0.9em; color: #666;"> <p style="margin: 0; font-size: 0.9em; color: #666;">
<strong>Примечание:</strong> Аватарка отображается на вашей публичной странице автора <strong>Примечание:</strong> Аватарка отображается на вашей публичной странице автора
</p> </p>
</div> </div>
<?php endif; ?> <?php endif; ?>
</article> </article>
</div> </div>
<article> <article>
<h3>Информация об аккаунте</h3> <h3>Информация об аккаунте</h3>
<p><a href="author.php?id=<?= $_SESSION['user_id'] ?>" target="_blank" class="adaptive-button secondary"> <p><a href="author.php?id=<?= $_SESSION['user_id'] ?>" target="_blank" class="adaptive-button secondary">
👁️ Посмотреть мою публичную страницу 👁️ Посмотреть мою публичную страницу
</a></p> </a></p>
<p><strong>Дата регистрации:</strong> <?= date('d.m.Y H:i', strtotime($user['created_at'])) ?></p> <p><strong>Дата регистрации:</strong> <?= date('d.m.Y H:i', strtotime($user['created_at'])) ?></p>
<?php if ($user['last_login']): ?> <?php if ($user['last_login']): ?>
<p><strong>Последний вход:</strong> <?= date('d.m.Y H:i', strtotime($user['last_login'])) ?></p> <p><strong>Последний вход:</strong> <?= date('d.m.Y H:i', strtotime($user['last_login'])) ?></p>
<?php endif; ?> <?php endif; ?>
</article> </article>
<?php include 'views/footer.php'; ?> <?php include 'views/footer.php'; ?>

188
series.php Executable file → Normal file
View File

@ -1,95 +1,95 @@
<?php <?php
require_once 'config/config.php'; require_once 'config/config.php';
require_login(); require_login();
$user_id = $_SESSION['user_id']; $user_id = $_SESSION['user_id'];
$seriesModel = new Series($pdo); $seriesModel = new Series($pdo);
$series = $seriesModel->findByUser($user_id); $series = $seriesModel->findByUser($user_id);
// Получаем статистику для каждой серии отдельно // Получаем статистику для каждой серии отдельно
foreach ($series as &$ser) { foreach ($series as &$ser) {
$stats = $seriesModel->getSeriesStats($ser['id'], $user_id); $stats = $seriesModel->getSeriesStats($ser['id'], $user_id);
$ser['book_count'] = $stats['book_count'] ?? 0; $ser['book_count'] = $stats['book_count'] ?? 0;
$ser['total_words'] = $stats['total_words'] ?? 0; $ser['total_words'] = $stats['total_words'] ?? 0;
} }
unset($ser); unset($ser);
$page_title = "Мои серии книг"; $page_title = "Мои серии книг";
include 'views/header.php'; include 'views/header.php';
?> ?>
<h1>Мои серии книг</h1> <h1>Мои серии книг</h1>
<?php if (isset($_SESSION['success'])): ?> <?php if (isset($_SESSION['success'])): ?>
<div class="alert alert-success"> <div class="alert alert-success">
<?= e($_SESSION['success']) ?> <?= e($_SESSION['success']) ?>
<?php unset($_SESSION['success']); ?> <?php unset($_SESSION['success']); ?>
</div> </div>
<?php endif; ?> <?php endif; ?>
<?php if (isset($_SESSION['error'])): ?> <?php if (isset($_SESSION['error'])): ?>
<div class="alert alert-error"> <div class="alert alert-error">
<?= e($_SESSION['error']) ?> <?= e($_SESSION['error']) ?>
<?php unset($_SESSION['error']); ?> <?php unset($_SESSION['error']); ?>
</div> </div>
<?php endif; ?> <?php endif; ?>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<h2 style="margin: 0;">Всего серий: <?= count($series) ?></h2> <h2 style="margin: 0;">Всего серий: <?= count($series) ?></h2>
<a href="series_edit.php" class="action-button primary"> Новая серия</a> <a href="series_edit.php" class="action-button primary"> Новая серия</a>
</div> </div>
<?php if (empty($series)): ?> <?php if (empty($series)): ?>
<article style="text-align: center; padding: 2rem;"> <article style="text-align: center; padding: 2rem;">
<h3>У вас пока нет серий книг</h3> <h3>У вас пока нет серий книг</h3>
<p>Создайте свою первую серию для организации книг!</p> <p>Создайте свою первую серию для организации книг!</p>
<a href="series_edit.php" role="button">📚 Создать первую серию</a> <a href="series_edit.php" role="button">📚 Создать первую серию</a>
</article> </article>
<?php else: ?> <?php else: ?>
<div class="grid"> <div class="grid">
<?php foreach ($series as $ser): ?> <?php foreach ($series as $ser): ?>
<article> <article>
<header> <header>
<h3> <h3>
<?= e($ser['title']) ?> <?= e($ser['title']) ?>
<div style="display: flex; gap: 3px; float:right;"> <div style="display: flex; gap: 3px; float:right;">
<a href="series_edit.php?id=<?= $ser['id'] ?>" class="compact-button secondary" title="Редактировать серию"> <a href="series_edit.php?id=<?= $ser['id'] ?>" class="compact-button secondary" title="Редактировать серию">
✏️ ✏️
</a> </a>
<a href="view_series.php?id=<?= $ser['id'] ?>" class="compact-button secondary" title="Просмотреть серию"> <a href="view_series.php?id=<?= $ser['id'] ?>" class="compact-button secondary" title="Просмотреть серию">
👁️ 👁️
</a> </a>
<form method="post" action="series_delete.php" style="display: inline;" onsubmit="return confirm('Вы уверены, что хотите удалить серию «<?= e($ser['title']) ?>»? Книги останутся, но будут убраны из серии.');"> <form method="post" action="series_delete.php" style="display: inline;" onsubmit="return confirm('Вы уверены, что хотите удалить серию «<?= e($ser['title']) ?>»? Книги останутся, но будут убраны из серии.');">
<input type="hidden" name="series_id" value="<?= $ser['id'] ?>"> <input type="hidden" name="series_id" value="<?= $ser['id'] ?>">
<input type="hidden" name="csrf_token" value="<?= generate_csrf_token() ?>"> <input type="hidden" name="csrf_token" value="<?= generate_csrf_token() ?>">
<button type="submit" class="compact-button secondary" style="background: #ff4444; border-color: #ff4444; color: white;" title="Удалить серию"> <button type="submit" class="compact-button secondary" style="background: #ff4444; border-color: #ff4444; color: white;" title="Удалить серию">
🗑️ 🗑️
</button> </button>
</form> </form>
</div> </div>
</h3> </h3>
</header> </header>
<?php if ($ser['description']): ?> <?php if ($ser['description']): ?>
<p><?= e(mb_strimwidth($ser['description'], 0, 200, '...')) ?></p> <p><?= e(mb_strimwidth($ser['description'], 0, 200, '...')) ?></p>
<?php endif; ?> <?php endif; ?>
<footer> <footer>
<div> <div>
<small> <small>
Книг: <?= $ser['book_count'] ?> | Книг: <?= $ser['book_count'] ?> |
Слов: <?= $ser['total_words'] ?> Слов: <?= $ser['total_words'] ?>
</small> </small>
</div> </div>
<div style="margin-top: 0.5rem;"> <div style="margin-top: 0.5rem;">
<a href="view_series.php?id=<?= $ser['id'] ?>" class="adaptive-button secondary"> <a href="view_series.php?id=<?= $ser['id'] ?>" class="adaptive-button secondary">
📖 Смотреть книги 📖 Смотреть книги
</a> </a>
</div> </div>
</footer> </footer>
</article> </article>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>
<?php endif; ?> <?php endif; ?>
<?php include 'views/footer.php'; ?> <?php include 'views/footer.php'; ?>

76
series_delete.php Executable file → Normal file
View File

@ -1,39 +1,39 @@
<?php <?php
require_once 'config/config.php'; require_once 'config/config.php';
require_login(); require_login();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
$_SESSION['error'] = "Неверный метод запроса"; $_SESSION['error'] = "Неверный метод запроса";
redirect('series.php'); redirect('series.php');
} }
if (!verify_csrf_token($_POST['csrf_token'] ?? '')) { if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
$_SESSION['error'] = "Ошибка безопасности"; $_SESSION['error'] = "Ошибка безопасности";
redirect('series.php'); redirect('series.php');
} }
$series_id = $_POST['series_id'] ?? null; $series_id = $_POST['series_id'] ?? null;
$user_id = $_SESSION['user_id']; $user_id = $_SESSION['user_id'];
if (!$series_id) { if (!$series_id) {
$_SESSION['error'] = "Не указана серия для удаления"; $_SESSION['error'] = "Не указана серия для удаления";
redirect('series.php'); redirect('series.php');
} }
$seriesModel = new Series($pdo); $seriesModel = new Series($pdo);
if (!$seriesModel->userOwnsSeries($series_id, $user_id)) { if (!$seriesModel->userOwnsSeries($series_id, $user_id)) {
$_SESSION['error'] = "У вас нет доступа к этой серии"; $_SESSION['error'] = "У вас нет доступа к этой серии";
redirect('series.php'); redirect('series.php');
} }
$series = $seriesModel->findById($series_id); $series = $seriesModel->findById($series_id);
if ($seriesModel->delete($series_id, $user_id)) { if ($seriesModel->delete($series_id, $user_id)) {
$_SESSION['success'] = "Серия «" . e($series['title']) . "» успешно удалена"; $_SESSION['success'] = "Серия «" . e($series['title']) . "» успешно удалена";
} else { } else {
$_SESSION['error'] = "Ошибка при удалении серии"; $_SESSION['error'] = "Ошибка при удалении серии";
} }
redirect('series.php'); redirect('series.php');
?> ?>

356
series_edit.php Executable file → Normal file
View File

@ -1,179 +1,179 @@
<?php <?php
require_once 'config/config.php'; require_once 'config/config.php';
require_login(); require_login();
$user_id = $_SESSION['user_id']; $user_id = $_SESSION['user_id'];
$seriesModel = new Series($pdo); $seriesModel = new Series($pdo);
$series_id = $_GET['id'] ?? null; $series_id = $_GET['id'] ?? null;
$series = null; $series = null;
$is_edit = false; $is_edit = false;
if ($series_id) { if ($series_id) {
$series = $seriesModel->findById($series_id); $series = $seriesModel->findById($series_id);
if (!$series || !$seriesModel->userOwnsSeries($series_id, $user_id)) { if (!$series || !$seriesModel->userOwnsSeries($series_id, $user_id)) {
$_SESSION['error'] = "Серия не найдена или у вас нет доступа"; $_SESSION['error'] = "Серия не найдена или у вас нет доступа";
redirect('series.php'); redirect('series.php');
} }
$is_edit = true; $is_edit = true;
} }
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!verify_csrf_token($_POST['csrf_token'] ?? '')) { if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
$_SESSION['error'] = "Ошибка безопасности"; $_SESSION['error'] = "Ошибка безопасности";
redirect($is_edit ? "series_edit.php?id=$series_id" : 'series_edit.php'); redirect($is_edit ? "series_edit.php?id=$series_id" : 'series_edit.php');
} }
$title = trim($_POST['title'] ?? ''); $title = trim($_POST['title'] ?? '');
$description = trim($_POST['description'] ?? ''); $description = trim($_POST['description'] ?? '');
if (empty($title)) { if (empty($title)) {
$_SESSION['error'] = "Название серии обязательно"; $_SESSION['error'] = "Название серии обязательно";
} else { } else {
$data = [ $data = [
'title' => $title, 'title' => $title,
'description' => $description, 'description' => $description,
'user_id' => $user_id 'user_id' => $user_id
]; ];
if ($is_edit) { if ($is_edit) {
$success = $seriesModel->update($series_id, $data); $success = $seriesModel->update($series_id, $data);
$message = $success ? "Серия успешно обновлена" : "Ошибка при обновлении серии"; $message = $success ? "Серия успешно обновлена" : "Ошибка при обновлении серии";
} else { } else {
$success = $seriesModel->create($data); $success = $seriesModel->create($data);
$message = $success ? "Серия успешно создана" : "Ошибка при создании серии"; $message = $success ? "Серия успешно создана" : "Ошибка при создании серии";
if ($success) { if ($success) {
$new_series_id = $pdo->lastInsertId(); $new_series_id = $pdo->lastInsertId();
redirect("series_edit.php?id=$new_series_id"); redirect("series_edit.php?id=$new_series_id");
} }
} }
if ($success) { if ($success) {
$_SESSION['success'] = $message; $_SESSION['success'] = $message;
redirect('series.php'); redirect('series.php');
} else { } else {
$_SESSION['error'] = $message; $_SESSION['error'] = $message;
} }
} }
} }
$page_title = $is_edit ? "Редактирование серии" : "Создание новой серии"; $page_title = $is_edit ? "Редактирование серии" : "Создание новой серии";
include 'views/header.php'; include 'views/header.php';
?> ?>
<h1><?= $is_edit ? "Редактирование серии" : "Создание новой серии" ?></h1> <h1><?= $is_edit ? "Редактирование серии" : "Создание новой серии" ?></h1>
<?php if (isset($_SESSION['error'])): ?> <?php if (isset($_SESSION['error'])): ?>
<div class="alert alert-error"> <div class="alert alert-error">
<?= e($_SESSION['error']) ?> <?= e($_SESSION['error']) ?>
<?php unset($_SESSION['error']); ?> <?php unset($_SESSION['error']); ?>
</div> </div>
<?php endif; ?> <?php endif; ?>
<form method="post"> <form method="post">
<input type="hidden" name="csrf_token" value="<?= generate_csrf_token() ?>"> <input type="hidden" name="csrf_token" value="<?= generate_csrf_token() ?>">
<div style="max-width: 100%; margin-bottom: 1rem;"> <div style="max-width: 100%; margin-bottom: 1rem;">
<label for="title" style="display: block; margin-bottom: 0.5rem; font-weight: bold;"> <label for="title" style="display: block; margin-bottom: 0.5rem; font-weight: bold;">
Название серии * Название серии *
</label> </label>
<input type="text" id="title" name="title" <input type="text" id="title" name="title"
value="<?= e($series['title'] ?? $_POST['title'] ?? '') ?>" value="<?= e($series['title'] ?? $_POST['title'] ?? '') ?>"
placeholder="Введите название серии" placeholder="Введите название серии"
style="width: 100%; margin-bottom: 1.5rem;" style="width: 100%; margin-bottom: 1.5rem;"
required> required>
<label for="description" style="display: block; margin-bottom: 0.5rem; font-weight: bold;"> <label for="description" style="display: block; margin-bottom: 0.5rem; font-weight: bold;">
Описание серии Описание серии
</label> </label>
<textarea id="description" name="description" <textarea id="description" name="description"
placeholder="Описание сюжета серии, общая концепция..." placeholder="Описание сюжета серии, общая концепция..."
rows="6" rows="6"
style="width: 100;"><?= e($series['description'] ?? $_POST['description'] ?? '') ?></textarea> style="width: 100;"><?= e($series['description'] ?? $_POST['description'] ?? '') ?></textarea>
</div> </div>
<div style="display: flex; gap: 10px; flex-wrap: wrap;"> <div style="display: flex; gap: 10px; flex-wrap: wrap;">
<button type="submit" class="contrast"> <button type="submit" class="contrast">
<?= $is_edit ? '💾 Сохранить изменения' : '📚 Создать серию' ?> <?= $is_edit ? '💾 Сохранить изменения' : '📚 Создать серию' ?>
</button> </button>
<a href="series.php" role="button" class="secondary"> <a href="series.php" role="button" class="secondary">
Отмена Отмена
</a> </a>
</div> </div>
</form> </form>
<?php if ($is_edit): ?> <?php if ($is_edit): ?>
<div style="margin-top: 3rem;"> <div style="margin-top: 3rem;">
<h3>Книги в этой серии</h3> <h3>Книги в этой серии</h3>
<?php <?php
$bookModel = new Book($pdo); $bookModel = new Book($pdo);
$books_in_series = $bookModel->findBySeries($series_id); $books_in_series = $bookModel->findBySeries($series_id);
// Вычисляем общую статистику // Вычисляем общую статистику
$total_chapters = 0; $total_chapters = 0;
$total_words = 0; $total_words = 0;
foreach ($books_in_series as $book) { foreach ($books_in_series as $book) {
$stats = $bookModel->getBookStats($book['id']); $stats = $bookModel->getBookStats($book['id']);
$total_chapters += $stats['chapter_count'] ?? 0; $total_chapters += $stats['chapter_count'] ?? 0;
$total_words += $stats['total_words'] ?? 0; $total_words += $stats['total_words'] ?? 0;
} }
?> ?>
<?php if (empty($books_in_series)): ?> <?php if (empty($books_in_series)): ?>
<div style="text-align: center; padding: 2rem; background: #f9f9f9; border-radius: 5px;"> <div style="text-align: center; padding: 2rem; background: #f9f9f9; border-radius: 5px;">
<p>В этой серии пока нет книг.</p> <p>В этой серии пока нет книг.</p>
<a href="books.php" class="adaptive-button">📚 Добавить книги</a> <a href="books.php" class="adaptive-button">📚 Добавить книги</a>
</div> </div>
<?php else: ?> <?php else: ?>
<div style="overflow-x: auto;"> <div style="overflow-x: auto;">
<table class="compact-table"> <table class="compact-table">
<thead> <thead>
<tr> <tr>
<th style="width: 10%;">Порядок</th> <th style="width: 10%;">Порядок</th>
<th style="width: 40%;">Название книги</th> <th style="width: 40%;">Название книги</th>
<th style="width: 20%;">Жанр</th> <th style="width: 20%;">Жанр</th>
<th style="width: 15%;">Статус</th> <th style="width: 15%;">Статус</th>
<th style="width: 15%;">Действия</th> <th style="width: 15%;">Действия</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<?php foreach ($books_in_series as $book): ?> <?php foreach ($books_in_series as $book): ?>
<tr> <tr>
<td><?= $book['sort_order_in_series'] ?></td> <td><?= $book['sort_order_in_series'] ?></td>
<td> <td>
<strong><?= e($book['title']) ?></strong> <strong><?= e($book['title']) ?></strong>
<?php if ($book['description']): ?> <?php if ($book['description']): ?>
<br><small style="color: #666;"><?= e(mb_strimwidth($book['description'], 0, 100, '...')) ?></small> <br><small style="color: #666;"><?= e(mb_strimwidth($book['description'], 0, 100, '...')) ?></small>
<?php endif; ?> <?php endif; ?>
</td> </td>
<td><?= e($book['genre']) ?></td> <td><?= e($book['genre']) ?></td>
<td> <td>
<span style="color: <?= $book['published'] ? 'green' : 'orange' ?>"> <span style="color: <?= $book['published'] ? 'green' : 'orange' ?>">
<?= $book['published'] ? '✅ Опубликована' : '📝 Черновик' ?> <?= $book['published'] ? '✅ Опубликована' : '📝 Черновик' ?>
</span> </span>
</td> </td>
<td> <td>
<a href="book_edit.php?id=<?= $book['id'] ?>" class="compact-button secondary"> <a href="book_edit.php?id=<?= $book['id'] ?>" class="compact-button secondary">
Редактировать Редактировать
</a> </a>
</td> </td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
</tbody> </tbody>
</table> </table>
</div> </div>
<div style="margin-top: 1rem; padding: 0.5rem; background: #f5f5f5; border-radius: 3px;"> <div style="margin-top: 1rem; padding: 0.5rem; background: #f5f5f5; border-radius: 3px;">
<strong>Статистика серии:</strong> <strong>Статистика серии:</strong>
Книг: <?= count($books_in_series) ?> | Книг: <?= count($books_in_series) ?> |
Глав: <?= $total_chapters ?> | Глав: <?= $total_chapters ?> |
Слов: <?= $total_words ?> Слов: <?= $total_words ?>
</div> </div>
<?php endif; ?> <?php endif; ?>
</div> </div>
<?php endif; ?> <?php endif; ?>
<?php include 'views/footer.php'; ?> <?php include 'views/footer.php'; ?>

View File

@ -1,270 +1,276 @@
<?php <?php
require_once 'config/config.php'; require_once 'config/config.php';
require_once 'includes/parsedown/ParsedownExtra.php'; require_once 'includes/parsedown/ParsedownExtra.php';
$Parsedown = new ParsedownExtra(); $Parsedown = new ParsedownExtra();
// Получаем книгу по share_token или id // Получаем книгу по share_token или id
$share_token = $_GET['share_token'] ?? null; $share_token = $_GET['share_token'] ?? null;
$book_id = $_GET['id'] ?? null; $book_id = $_GET['id'] ?? null;
$bookModel = new Book($pdo); $bookModel = new Book($pdo);
$book = null; $book = null;
if ($share_token) { if ($share_token) {
$book = $bookModel->findByShareToken($share_token); $book = $bookModel->findByShareToken($share_token);
} elseif ($book_id) { } elseif ($book_id) {
$book = $bookModel->findById($book_id); $book = $bookModel->findById($book_id);
} }
if (!$book) { if (!$book) {
http_response_code(404); http_response_code(404);
$page_title = "Книга не найдена"; $page_title = "Книга не найдена";
include 'views/header.php'; include 'views/header.php';
?> ?>
<div class="container"> <div class="container">
<article style="text-align: center; padding: 2rem;"> <article style="text-align: center; padding: 2rem;">
<h1>Книга не найдена</h1> <h1>Книга не найдена</h1>
<p>Запрошенная книга не существует или была удалена.</p> <p>Запрошенная книга не существует или была удалена.</p>
<a href="index.php" role="button">На главную</a> <a href="index.php" role="button">На главную</a>
</article> </article>
</div> </div>
<?php <?php
include 'views/footer.php'; include 'views/footer.php';
exit; exit;
} }
// Получаем опубликованные главы // Получаем опубликованные главы
$chapters = $bookModel->getPublishedChapters($book['id']); $chapters = $bookModel->getPublishedChapters($book['id']);
$total_words = array_sum(array_column($chapters, 'word_count')); $total_words = array_sum(array_column($chapters, 'word_count'));
// Получаем информацию об авторе // Получаем информацию об авторе
$stmt = $pdo->prepare("SELECT display_name, username FROM users WHERE id = ?"); $stmt = $pdo->prepare("SELECT display_name, username FROM users WHERE id = ?");
$stmt->execute([$book['user_id']]); $stmt->execute([$book['user_id']]);
$author_info = $stmt->fetch(PDO::FETCH_ASSOC); $author_info = $stmt->fetch(PDO::FETCH_ASSOC);
$author_name = $author_info['display_name'] ?? $author_info['username'] ?? 'Неизвестный автор'; $author_name = $author_info['display_name'] ?? $author_info['username'] ?? 'Неизвестный автор';
$page_title = $book['title']; $page_title = $book['title'];
include 'views/header.php'; include 'views/header.php';
?> ?>
<div class="container"> <div class="container">
<article style="max-width: 800px; margin: 0 auto;"> <article style="max-width: 800px; margin: 0 auto;">
<header style="text-align: center; margin-bottom: 2rem; border-bottom: 2px solid #eee; padding-bottom: 1rem;"> <header style="text-align: center; margin-bottom: 2rem; border-bottom: 2px solid #eee; padding-bottom: 1rem;">
<?php if ($book['cover_image']): ?> <?php if ($book['cover_image']): ?>
<div style="margin-bottom: 1rem;"> <div style="margin-bottom: 1rem;">
<img src="<?= COVERS_URL . e($book['cover_image']) ?>" <img src="<?= COVERS_URL . e($book['cover_image']) ?>"
alt="<?= e($book['title']) ?>" alt="<?= e($book['title']) ?>"
style="max-width: 200px; height: auto; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);" style="max-width: 200px; height: auto; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);"
onerror="this.style.display='none'"> onerror="this.style.display='none'">
</div> </div>
<?php endif; ?> <?php endif; ?>
<h1 style="margin-bottom: 0.5rem;"><?= e($book['title']) ?></h1> <h1 style="margin-bottom: 0.5rem;"><?= e($book['title']) ?></h1>
<?php if ($book['series_id']): ?> <?php if ($book['series_id']): ?>
<?php <?php
$series_stmt = $pdo->prepare("SELECT id, title FROM series WHERE id = ?"); $series_stmt = $pdo->prepare("SELECT id, title FROM series WHERE id = ?");
$series_stmt->execute([$book['series_id']]); $series_stmt->execute([$book['series_id']]);
$series = $series_stmt->fetch(); $series = $series_stmt->fetch();
?> ?>
<?php if ($series): ?> <?php if ($series): ?>
<p style="color: #666; margin-bottom: 0.5rem;"> <p style="color: #666; margin-bottom: 0.5rem;">
📚 Часть серии: 📚 Часть серии:
<a href="view_series.php?id=<?= $series['id'] ?>" style="color: #007bff;"> <a href="view_series.php?id=<?= $series['id'] ?>" style="color: #007bff;">
<?= e($series['title']) ?> <?= e($series['title']) ?>
<?php if ($book['sort_order_in_series']): ?> <?php if ($book['sort_order_in_series']): ?>
(Книга <?= $book['sort_order_in_series'] ?>) (Книга <?= $book['sort_order_in_series'] ?>)
<?php endif; ?> <?php endif; ?>
</a> </a>
</p> </p>
<?php endif; ?> <?php endif; ?>
<?php endif; ?> <?php endif; ?>
<p style="color: #666; font-style: italic; margin-bottom: 0.5rem;"><?= e($author_name) ?></p> <p style="color: #666; font-style: italic; margin-bottom: 0.5rem;"><?= e($author_name) ?></p>
<?php if ($book['genre']): ?> <?php if ($book['genre']): ?>
<p style="color: #666; font-style: italic; margin-bottom: 0.5rem;"> <p style="color: #666; font-style: italic; margin-bottom: 0.5rem;">
Жанр: <?= e($book['genre']) ?> Жанр: <?= e($book['genre']) ?>
</p> </p>
<?php endif; ?> <?php endif; ?>
<?php if ($book['description']): ?> <?php if ($book['description']): ?>
<div style="background: #f8f9fa; padding: 1rem; border-radius: 5px; margin: 1rem 0;"> <div style="background: #f8f9fa; padding: 1rem; border-radius: 5px; margin: 1rem 0;">
<p style="margin: 0; font-size: 1.1em;"><?= nl2br(e($book['description'])) ?></p> <p style="margin: 0; font-size: 1.1em;"><?= nl2br(e($book['description'])) ?></p>
</div> </div>
<?php endif; ?> <?php endif; ?>
<div style="display: flex; justify-content: center; gap: 1rem; flex-wrap: wrap; font-size: 0.9em; color: #666;"> <div style="display: flex; justify-content: center; gap: 1rem; flex-wrap: wrap; font-size: 0.9em; color: #666;">
<span>Глав: <?= count($chapters) ?></span> <span>Глав: <?= count($chapters) ?></span>
<span>Слов: <?= $total_words ?></span> <span>Слов: <?= $total_words ?></span>
<?php if (is_logged_in() && $book['user_id'] == $_SESSION['user_id']): ?> <?php if (is_logged_in() && $book['user_id'] == $_SESSION['user_id']): ?>
<span>|</span> <span>|</span>
<a href="books.php" style="color: #007bff;">Вернуться к редактированию</a> <a href="books.php" style="color: #007bff;">Вернуться к редактированию</a>
<?php endif; ?> <?php endif; ?>
</div> </div>
</header> </header>
<!-- Интерактивное оглавление --> <!-- Интерактивное оглавление -->
<?php if (!empty($chapters)): ?> <?php if (!empty($chapters)): ?>
<div style="margin: 2rem 0; padding: 1.5rem; background: #f8f9fa; border-radius: 8px; border-left: 4px solid #007bff;"> <div style="margin: 2rem 0; padding: 1.5rem; background: #f8f9fa; border-radius: 8px; border-left: 4px solid #007bff;">
<h3 style="margin-top: 0; color: #007bff;">📖 Оглавление</h3> <h3 style="margin-top: 0; color: #007bff;">📖 Оглавление</h3>
<a name="start"></a> <a name="start"></a>
<div style="columns: 1;"> <div style="columns: 1;">
<?php foreach ($chapters as $index => $chapter): ?> <?php foreach ($chapters as $index => $chapter): ?>
<div style="break-inside: avoid; margin-bottom: 0.5rem;"> <div style="break-inside: avoid; margin-bottom: 0.5rem;">
<a href="#chapter-<?= $chapter['id'] ?>" <a href="#chapter-<?= $chapter['id'] ?>"
style="text-decoration: none; color: #333; display: block; padding: 0.3rem 0;" style="text-decoration: none; color: #333; display: block; padding: 0.3rem 0;"
onmouseover="this.style.color='#007bff'" onmouseover="this.style.color='#007bff'"
onmouseout="this.style.color='#333'"> onmouseout="this.style.color='#333'">
<span style="color: #666; font-size: 0.9em;"><?= $index + 1 ?>.</span> <span style="color: #666; font-size: 0.9em;"><?= $index + 1 ?>.</span>
<?= e($chapter['title']) ?> <?= e($chapter['title']) ?>
</a> </a>
</div> </div>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>
</div> </div>
<?php endif; ?> <?php endif; ?>
<div style="margin: 1rem 0; padding: 1rem; background: #f8f9fa; border-radius: 5px;"> <div style="margin: 1rem 0; padding: 1rem; background: #f8f9fa; border-radius: 5px;">
<h3 style="margin: 0 0 0.5rem 0;">Экспорт книги</h3> <h3 style="margin: 0 0 0.5rem 0;">Экспорт книги</h3>
<div style="display: flex; gap: 5px; flex-wrap: wrap;"> <div style="display: flex; gap: 5px; flex-wrap: wrap;">
<a href="export_book.php?share_token=<?= $book['share_token'] ?>&format=pdf" class="adaptive-button secondary" target="_blank"> <a href="export_book.php?share_token=<?= $book['share_token'] ?>&format=pdf" class="adaptive-button secondary" target="_blank">
📄 PDF 📄 PDF
</a> </a>
<a href="export_book.php?share_token=<?= $book['share_token'] ?>&format=docx" class="adaptive-button secondary" target="_blank"> <a href="export_book.php?share_token=<?= $book['share_token'] ?>&format=docx" class="adaptive-button secondary" target="_blank">
📝 DOCX 📝 DOCX
</a> </a>
<a href="export_book.php?share_token=<?= $book['share_token'] ?>&format=html" class="adaptive-button secondary" target="_blank"> <a href="export_book.php?share_token=<?= $book['share_token'] ?>&format=html" class="adaptive-button secondary" target="_blank">
🌐 HTML 🌐 HTML
</a> </a>
<a href="export_book.php?share_token=<?= $book['share_token'] ?>&format=txt" class="adaptive-button secondary" target="_blank"> <a href="export_book.php?share_token=<?= $book['share_token'] ?>&format=txt" class="adaptive-button secondary" target="_blank">
📄 TXT 📄 TXT
</a> </a>
</div> </div>
<p style="margin-top: 0.5rem; font-size: 0.9em; color: #666;"> <p style="margin-top: 0.5rem; font-size: 0.9em; color: #666;">
<strong>Примечание:</strong> Экспортируются только опубликованные главы <strong>Примечание:</strong> Экспортируются только опубликованные главы
</p> </p>
</div> </div>
<?php if (empty($chapters)): ?> <?php if (empty($chapters)): ?>
<div style="text-align: center; padding: 3rem; background: #f9f9f9; border-radius: 5px;"> <div style="text-align: center; padding: 3rem; background: #f9f9f9; border-radius: 5px;">
<h3>В этой книге пока нет опубликованных глав</h3> <h3>В этой книге пока нет опубликованных глав</h3>
<p>Автор еще не опубликовал ни одной главы</p> <p>Автор еще не опубликовал ни одной главы</p>
</div> </div>
<?php else: ?> <?php else: ?>
<div class="book-content"> <div class="book-content">
<?php foreach ($chapters as $index => $chapter): ?> <?php foreach ($chapters as $index => $chapter): ?>
<section class="chapter" id="chapter-<?= $chapter['id'] ?>" style="margin-bottom: 3rem; scroll-margin-top: 2rem;"> <section class="chapter" id="chapter-<?= $chapter['id'] ?>" style="margin-bottom: 3rem; scroll-margin-top: 2rem;">
<h2 style="border-bottom: 1px solid #eee; padding-bottom: 0.5rem;"> <h2 style="border-bottom: 1px solid #eee; padding-bottom: 0.5rem;">
<?= e($chapter['title']) ?> <?= e($chapter['title']) ?>
<a href="#start" style="text-decoration: none; color: #666; font-size: 0.8em; margin-left: 1rem;">🔗</a> <a href="#start" style="text-decoration: none; color: #666; font-size: 0.8em; margin-left: 1rem;">🔗</a>
</h2> </h2>
<div class="chapter-content" style="line-height: 1.6; font-size: 1.1em;"> <div class="chapter-content" style="line-height: 1.6; font-size: 1.1em;">
<?= $Parsedown->text($chapter['content']) ?> <?php if ($book['editor_type'] == 'markdown'): ?>
</div> <?= $Parsedown->text($chapter['content']) ?>
<div style="margin-top: 1rem; padding-top: 0.5rem; border-top: 1px dashed #eee; color: #666; font-size: 0.9em;"> <?php else: ?>
<small>Обновлено: <?= date('d.m.Y', strtotime($chapter['updated_at'])) ?></small> <?= $chapter['content'] ?>
<a href="#top" style="float: right; color: #007bff; text-decoration: none;"> Наверх</a> <?php endif; ?>
</div> </div>
</section>
<?php endforeach; ?>
</div> <div style="margin-top: 1rem; padding-top: 0.5rem; border-top: 1px dashed #eee; color: #666; font-size: 0.9em;">
<?php endif; ?> <small>Обновлено: <?= date('d.m.Y', strtotime($chapter['updated_at'])) ?></small>
<a href="#top" style="float: right; color: #007bff; text-decoration: none;"> Наверх</a>
<footer style="margin-top: 3rem; padding-top: 1rem; border-top: 2px solid #eee; text-align: center;"> </div>
<p style="color: #666;"> </section>
Книга создана в <?= e(APP_NAME) ?> <?php endforeach; ?>
Автор: <?= e($author_name) ?> </div>
<?= date('d.m.Y', strtotime($book['created_at'])) ?> <?php endif; ?>
</p>
</footer> <footer style="margin-top: 3rem; padding-top: 1rem; border-top: 2px solid #eee; text-align: center;">
</article> <p style="color: #666;">
</div> Книга создана в <?= e(APP_NAME) ?>
Автор: <?= e($author_name) ?>
<style> <?= date('d.m.Y', strtotime($book['created_at'])) ?>
.book-content { </p>
line-height: 1.7; </footer>
} </article>
</div>
.book-content h1, .book-content h2, .book-content h3, .book-content h4, .book-content h5, .book-content h6 {
margin-top: 2rem; <style>
margin-bottom: 1rem; .book-content {
} line-height: 1.7;
}
.book-content p {
margin-bottom: 1rem; .book-content h1, .book-content h2, .book-content h3, .book-content h4, .book-content h5, .book-content h6 {
text-align: justify; margin-top: 2rem;
} margin-bottom: 1rem;
}
.book-content blockquote {
border-left: 4px solid #007bff; .book-content p {
padding-left: 1rem; margin-bottom: 1rem;
margin-left: 0; text-align: justify;
color: #555; }
font-style: italic;
} .book-content blockquote {
border-left: 4px solid #007bff;
.book-content code { padding-left: 1rem;
background: #f5f5f5; margin-left: 0;
padding: 2px 4px; color: #555;
border-radius: 3px; font-style: italic;
} }
.book-content pre { .book-content code {
background: #f5f5f5; background: #f5f5f5;
padding: 1rem; padding: 2px 4px;
border-radius: 5px; border-radius: 3px;
overflow-x: auto; }
}
.book-content pre {
.book-content ul, .book-content ol { background: #f5f5f5;
margin-bottom: 1rem; padding: 1rem;
padding-left: 2rem; border-radius: 5px;
} overflow-x: auto;
}
.book-content table {
width: 100%; .book-content ul, .book-content ol {
border-collapse: collapse; margin-bottom: 1rem;
margin-bottom: 1rem; padding-left: 2rem;
} }
.book-content th, .book-content td { .book-content table {
border: 1px solid #ddd; width: 100%;
padding: 8px 12px; border-collapse: collapse;
text-align: left; margin-bottom: 1rem;
} }
.book-content th { .book-content th, .book-content td {
background: #f5f5f5; border: 1px solid #ddd;
} padding: 8px 12px;
text-align: left;
@media (max-width: 768px) { }
.book-content {
font-size: 16px; .book-content th {
line-height: 1.6; background: #f5f5f5;
} }
.book-content h1 { @media (max-width: 768px) {
font-size: 1.6em; .book-content {
} font-size: 16px;
line-height: 1.6;
.book-content h2 { }
font-size: 1.4em;
} .book-content h1 {
font-size: 1.6em;
.book-content h3 { }
font-size: 1.2em;
} .book-content h2 {
font-size: 1.4em;
.book-content pre { }
font-size: 14px;
} .book-content h3 {
font-size: 1.2em;
div[style*="columns: 2"] { }
columns: 1 !important;
} .book-content pre {
} font-size: 14px;
</style> }
div[style*="columns: 2"] {
columns: 1 !important;
}
}
</style>
<?php include 'views/footer.php'; ?> <?php include 'views/footer.php'; ?>

320
view_series.php Executable file → Normal file
View File

@ -1,161 +1,161 @@
<?php <?php
require_once 'config/config.php'; require_once 'config/config.php';
require_once 'includes/parsedown/ParsedownExtra.php'; require_once 'includes/parsedown/ParsedownExtra.php';
$Parsedown = new ParsedownExtra(); $Parsedown = new ParsedownExtra();
$series_id = (int)($_GET['id'] ?? 0); $series_id = (int)($_GET['id'] ?? 0);
if (!$series_id) { if (!$series_id) {
http_response_code(400); http_response_code(400);
echo "<h2>Неверный запрос</h2>"; echo "<h2>Неверный запрос</h2>";
include 'views/footer.php'; include 'views/footer.php';
exit; exit;
} }
$seriesModel = new Series($pdo); $seriesModel = new Series($pdo);
$series = $seriesModel->findById($series_id); $series = $seriesModel->findById($series_id);
if (!$series) { if (!$series) {
http_response_code(404); http_response_code(404);
echo "<h2>Серия не найдена</h2>"; echo "<h2>Серия не найдена</h2>";
include 'views/footer.php'; include 'views/footer.php';
exit; exit;
} }
// Получаем только опубликованные книги серии // Получаем только опубликованные книги серии
$books = $seriesModel->getBooksInSeries($series_id, true); $books = $seriesModel->getBooksInSeries($series_id, true);
// Получаем информацию об авторе // Получаем информацию об авторе
$stmt = $pdo->prepare("SELECT id, username, display_name FROM users WHERE id = ?"); $stmt = $pdo->prepare("SELECT id, username, display_name FROM users WHERE id = ?");
$stmt->execute([$series['user_id']]); $stmt->execute([$series['user_id']]);
$author = $stmt->fetch(PDO::FETCH_ASSOC); $author = $stmt->fetch(PDO::FETCH_ASSOC);
// Получаем статистику по опубликованным книгам // Получаем статистику по опубликованным книгам
$bookModel = new Book($pdo); $bookModel = new Book($pdo);
$total_words = 0; $total_words = 0;
$total_chapters = 0; $total_chapters = 0;
foreach ($books as $book) { foreach ($books as $book) {
$book_stats = $bookModel->getBookStats($book['id'], true); // true - только опубликованные главы $book_stats = $bookModel->getBookStats($book['id'], true); // true - только опубликованные главы
$total_words += $book_stats['total_words'] ?? 0; $total_words += $book_stats['total_words'] ?? 0;
$total_chapters += $book_stats['chapter_count'] ?? 0; $total_chapters += $book_stats['chapter_count'] ?? 0;
} }
$page_title = $series['title'] . ' — серия книг'; $page_title = $series['title'] . ' — серия книг';
include 'views/header.php'; include 'views/header.php';
?> ?>
<div class="container"> <div class="container">
<article style="max-width: 800px; margin: 0 auto;"> <article style="max-width: 800px; margin: 0 auto;">
<header style="text-align: center; margin-bottom: 2rem; border-bottom: 2px solid #eee; padding-bottom: 1rem;"> <header style="text-align: center; margin-bottom: 2rem; border-bottom: 2px solid #eee; padding-bottom: 1rem;">
<h1 style="margin-bottom: 0.5rem;"><?= e($series['title']) ?></h1> <h1 style="margin-bottom: 0.5rem;"><?= e($series['title']) ?></h1>
<p style="color: #666; font-style: italic; margin-bottom: 0.5rem;"> <p style="color: #666; font-style: italic; margin-bottom: 0.5rem;">
Серия книг от Серия книг от
<a href="author.php?id=<?= $author['id'] ?>"><?= e($author['display_name'] ?: $author['username']) ?></a> <a href="author.php?id=<?= $author['id'] ?>"><?= e($author['display_name'] ?: $author['username']) ?></a>
</p> </p>
<?php if ($series['description']): ?> <?php if ($series['description']): ?>
<div style="background: #f8f9fa; padding: 1rem; border-radius: 5px; margin: 1rem 0; text-align: left;"> <div style="background: #f8f9fa; padding: 1rem; border-radius: 5px; margin: 1rem 0; text-align: left;">
<?= $Parsedown->text($series['description']) ?> <?= $Parsedown->text($series['description']) ?>
</div> </div>
<?php endif; ?> <?php endif; ?>
<div style="display: flex; justify-content: center; gap: 1rem; flex-wrap: wrap; font-size: 0.9em; color: #666;"> <div style="display: flex; justify-content: center; gap: 1rem; flex-wrap: wrap; font-size: 0.9em; color: #666;">
<span>Книг: <?= count($books) ?></span> <span>Книг: <?= count($books) ?></span>
<span>Глав: <?= $total_chapters ?></span> <span>Глав: <?= $total_chapters ?></span>
<span>Слов: <?= $total_words ?></span> <span>Слов: <?= $total_words ?></span>
</div> </div>
</header> </header>
<?php if (empty($books)): ?> <?php if (empty($books)): ?>
<div style="text-align: center; padding: 3rem; background: #f9f9f9; border-radius: 5px;"> <div style="text-align: center; padding: 3rem; background: #f9f9f9; border-radius: 5px;">
<h3>В этой серии пока нет опубликованных книг</h3> <h3>В этой серии пока нет опубликованных книг</h3>
<p>Автор еще не опубликовал книги из этой серии</p> <p>Автор еще не опубликовал книги из этой серии</p>
</div> </div>
<?php else: ?> <?php else: ?>
<div class="series-books"> <div class="series-books">
<h2 style="text-align: center; margin-bottom: 2rem;">Книги серии</h2> <h2 style="text-align: center; margin-bottom: 2rem;">Книги серии</h2>
<?php foreach ($books as $book): ?> <?php foreach ($books as $book): ?>
<article style="display: flex; gap: 1rem; align-items: flex-start; margin-bottom: 2rem; padding: 1rem; background: #f8f9fa; border-radius: 8px;"> <article style="display: flex; gap: 1rem; align-items: flex-start; margin-bottom: 2rem; padding: 1rem; background: #f8f9fa; border-radius: 8px;">
<?php if ($book['cover_image']): ?> <?php if ($book['cover_image']): ?>
<div style="flex-shrink: 0;"> <div style="flex-shrink: 0;">
<img src="<?= COVERS_URL . e($book['cover_image']) ?>" <img src="<?= COVERS_URL . e($book['cover_image']) ?>"
alt="<?= e($book['title']) ?>" alt="<?= e($book['title']) ?>"
style="max-width: 120px; height: auto; border-radius: 4px; border: 1px solid #ddd;" style="max-width: 120px; height: auto; border-radius: 4px; border: 1px solid #ddd;"
onerror="this.style.display='none'"> onerror="this.style.display='none'">
</div> </div>
<?php else: ?> <?php else: ?>
<div style="flex-shrink: 0;"> <div style="flex-shrink: 0;">
<div class="cover-placeholder" style="width: 120px; height: 160px;">📚</div> <div class="cover-placeholder" style="width: 120px; height: 160px;">📚</div>
</div> </div>
<?php endif; ?> <?php endif; ?>
<div style="flex: 1;"> <div style="flex: 1;">
<h3 style="margin-top: 0;"> <h3 style="margin-top: 0;">
<?php if ($book['sort_order_in_series']): ?> <?php if ($book['sort_order_in_series']): ?>
<small style="color: #666;">Книга <?= $book['sort_order_in_series'] ?></small><br> <small style="color: #666;">Книга <?= $book['sort_order_in_series'] ?></small><br>
<?php endif; ?> <?php endif; ?>
<?= e($book['title']) ?> <?= e($book['title']) ?>
</h3> </h3>
<?php if ($book['genre']): ?> <?php if ($book['genre']): ?>
<p style="color: #666; margin: 0.5rem 0;"><em><?= e($book['genre']) ?></em></p> <p style="color: #666; margin: 0.5rem 0;"><em><?= e($book['genre']) ?></em></p>
<?php endif; ?> <?php endif; ?>
<?php if ($book['description']): ?> <?php if ($book['description']): ?>
<p style="margin-bottom: 1rem;"><?= nl2br(e($book['description'])) ?></p> <p style="margin-bottom: 1rem;"><?= nl2br(e($book['description'])) ?></p>
<?php endif; ?> <?php endif; ?>
<div style="display: flex; gap: 10px; flex-wrap: wrap; align-items: center;"> <div style="display: flex; gap: 10px; flex-wrap: wrap; align-items: center;">
<a href="view_book.php?share_token=<?= e($book['share_token']) ?>" class="adaptive-button"> <a href="view_book.php?share_token=<?= e($book['share_token']) ?>" class="adaptive-button">
Читать Читать
</a> </a>
<?php <?php
$bookModel = new Book($pdo); $bookModel = new Book($pdo);
$book_stats = $bookModel->getBookStats($book['id'], true); // true - только опубликованные главы $book_stats = $bookModel->getBookStats($book['id'], true); // true - только опубликованные главы
?> ?>
<small style="color: #666;"> <small style="color: #666;">
Глав: <?= $book_stats['chapter_count'] ?? 0 ?> | Слов: <?= $book_stats['total_words'] ?? 0 ?> Глав: <?= $book_stats['chapter_count'] ?? 0 ?> | Слов: <?= $book_stats['total_words'] ?? 0 ?>
</small> </small>
</div> </div>
</div> </div>
</article> </article>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>
<?php endif; ?> <?php endif; ?>
<footer style="margin-top: 3rem; padding-top: 1rem; border-top: 2px solid #eee; text-align: center;"> <footer style="margin-top: 3rem; padding-top: 1rem; border-top: 2px solid #eee; text-align: center;">
<p style="color: #666;"> <p style="color: #666;">
Серия создана в <?= e(APP_NAME) ?> Серия создана в <?= e(APP_NAME) ?>
Автор: <a href="author.php?id=<?= $author['id'] ?>"><?= e($author['display_name'] ?: $author['username']) ?></a> Автор: <a href="author.php?id=<?= $author['id'] ?>"><?= e($author['display_name'] ?: $author['username']) ?></a>
</p> </p>
</footer> </footer>
</article> </article>
</div> </div>
<style> <style>
.series-books article { .series-books article {
transition: transform 0.2s ease, box-shadow 0.2s ease; transition: transform 0.2s ease, box-shadow 0.2s ease;
} }
.series-books article:hover { .series-books article:hover {
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1); box-shadow: 0 4px 12px rgba(0,0,0,0.1);
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.series-books article { .series-books article {
flex-direction: column; flex-direction: column;
text-align: center; text-align: center;
} }
.series-books .book-cover { .series-books .book-cover {
align-self: center; align-self: center;
} }
} }
</style> </style>
<?php include 'views/footer.php'; ?> <?php include 'views/footer.php'; ?>

View File

@ -1,51 +1,64 @@
<?php <?php
// views/header.php // views/header.php
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="ru"> <html lang="ru">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= e(APP_NAME) ?> - <?= e($page_title ?? 'Платформа для писателей') ?></title> <title><?= e(APP_NAME) ?> - <?= e($page_title ?? 'Платформа для писателей') ?></title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@1.5.10/css/pico.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@1.5.10/css/pico.min.css">
<link rel="stylesheet" href="/assets/css/style.css"> <link rel="stylesheet" href="/assets/css/style.css">
<link rel="stylesheet" href="/assets/css/foundation-icons.css" /> <link rel="stylesheet" href="/assets/css/foundation-icons.css" />
</head> </head>
<body> <body>
<nav class="container-fluid black"> <nav class="container-fluid black">
<ul> <ul>
<li><strong><a href="/" style="text-decoration: none;"><?= e(APP_NAME) ?></a></strong></li> <li><strong><a href="/" style="text-decoration: none;"><?= e(APP_NAME) ?></a></strong></li>
</ul> </ul>
<ul> <ul>
<?php if (is_logged_in()): ?> <?php if (is_logged_in()): ?>
<li><a href="/dashboard.php">📊 Панель</a></li> <li><a href="/dashboard.php">📊 Панель</a></li>
<li><a href="/series.php">📚 Мои серии</a></li> <li><a href="/series.php">📚 Мои серии</a></li>
<li><a href="/books.php">📚 Мои книги</a></li> <li><a href="/books.php">📚 Мои книги</a></li>
<li> <li>
<details role="list" dir="rtl"> <details role="list" dir="rtl">
<summary aria-haspopup="listbox" role="link" style="display: flex; align-items: center; gap: 0.5rem;"> <summary aria-haspopup="listbox" role="link" style="display: flex; align-items: center; gap: 0.5rem;">
<?php if (!empty($_SESSION['avatar'])): ?> <?php if (!empty($_SESSION['avatar'])): ?>
<img src="<?= AVATARS_URL . e($_SESSION['avatar']) ?>" <img src="<?= AVATARS_URL . e($_SESSION['avatar']) ?>"
alt="Аватар" alt="Аватар"
style="width: 32px; height: 32px; border-radius: 50%; object-fit: cover;" style="width: 32px; height: 32px; border-radius: 50%; object-fit: cover;"
onerror="this.style.display='none'"> onerror="this.style.display='none'">
<?php endif; ?> <?php endif; ?>
👤 <?= e($_SESSION['display_name']) ?> 👤 <?= e($_SESSION['display_name']) ?>
</summary> </summary>
<ul role="listbox"> <ul role="listbox">
<li><a href="/profile.php">Настройки профиля</a></li> <li><a href="/profile.php">Настройки профиля</a></li>
<li><a href="/author.php?id=<?= $_SESSION['user_id'] ?>" target="_blank">Моя публичная страница</a></li> <li><a href="/author.php?id=<?= $_SESSION['user_id'] ?>" target="_blank">Моя публичная страница</a></li>
<?php if ($_SESSION['user_id'] == 1): ?> <?php if ($_SESSION['user_id'] == 1): ?>
<li><a href="/admin/users.php">👥 Пользователи</a></li> <li><a href="/admin/users.php">👥 Пользователи</a></li>
<?php endif; ?> <?php endif; ?>
<li><a href="/logout.php">Выйти</a></li> <li><a href="/logout.php">Выйти</a></li>
</ul> </ul>
</details> </details>
</li> </li>
<?php else: ?> <?php else: ?>
<li><a href="/login.php">Войти</a></li> <li><a href="/login.php">Войти</a></li>
<li><a href="/register.php">Регистрация</a></li> <li><a href="/register.php">Регистрация</a></li>
<?php endif; ?> <?php endif; ?>
</ul> </ul>
</nav> </nav>
<main class="container"> <main class="container">
<?php if (isset($_SESSION['info'])): ?>
<div class="alert alert-info">
<?= e($_SESSION['info']) ?>
<?php unset($_SESSION['info']); ?>
</div>
<?php endif; ?>
<?php if (isset($_SESSION['warning'])): ?>
<div class="alert alert-warning">
<?= e($_SESSION['warning']) ?>
<?php unset($_SESSION['warning']); ?>
</div>
<?php endif; ?>