add wisiwig editor TinyMCE. default content type = markdown.
This commit is contained in:
parent
d97e4d4944
commit
d7fe90a615
174
README.md
174
README.md
|
|
@ -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).
|
||||||
1882
assets/css/style.css
1882
assets/css/style.css
File diff suppressed because it is too large
Load Diff
382
author.php
382
author.php
|
|
@ -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'; ?>
|
||||||
|
|
@ -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');
|
||||||
?>
|
?>
|
||||||
|
|
@ -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');
|
||||||
?>
|
?>
|
||||||
757
book_edit.php
757
book_edit.php
|
|
@ -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>
|
||||||
|
|
||||||
<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>
|
||||||
|
|
||||||
|
<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
310
books.php
|
|
@ -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'; ?>
|
||||||
|
|
@ -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");
|
||||||
?>
|
?>
|
||||||
675
chapter_edit.php
675
chapter_edit.php
|
|
@ -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'; ?>
|
||||||
238
chapters.php
238
chapters.php
|
|
@ -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'; ?>
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"require": {
|
"require": {
|
||||||
"phpoffice/phpword": "^1.0",
|
"phpoffice/phpword": "^1.0",
|
||||||
"tecnickcom/tcpdf": "^6.6"
|
"tecnickcom/tcpdf": "^6.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
380
dashboard.php
380
dashboard.php
|
|
@ -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'; ?>
|
||||||
1632
export_book.php
1632
export_book.php
File diff suppressed because it is too large
Load Diff
|
|
@ -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
198
login.php
|
|
@ -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'; ?>
|
||||||
701
models/Book.php
701
models/Book.php
|
|
@ -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', '', $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);
|
||||||
|
}
|
||||||
|
}
|
||||||
?>
|
?>
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
236
models/User.php
236
models/User.php
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
10
preview.php
10
preview.php
|
|
@ -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);
|
||||||
?>
|
?>
|
||||||
|
|
|
||||||
388
profile.php
388
profile.php
|
|
@ -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'; ?>
|
||||||
|
|
@ -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'; ?>
|
||||||
|
|
@ -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');
|
||||||
?>
|
?>
|
||||||
|
|
@ -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'; ?>
|
||||||
544
view_book.php
544
view_book.php
|
|
@ -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'; ?>
|
||||||
|
|
@ -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'; ?>
|
||||||
115
views/header.php
115
views/header.php
|
|
@ -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; ?>
|
||||||
Loading…
Reference in New Issue