many fixes in design and add shirt story type for creation book

This commit is contained in:
2026-05-01 20:51:42 +03:00
parent 9294399517
commit ce6fffaa57
24 changed files with 1167 additions and 735 deletions
+62 -2
View File
@@ -34,7 +34,7 @@ include 'views/layouts/header.php';
<div class="card">
<div class="card-body">
<form method="post" enctype="multipart/form-data">
<form method="post" enctype="multipart/form-data" id="book-form">
<input type="hidden" name="csrf_token" value="<?= generate_csrf_token() ?>">
<div class="mb-3">
@@ -94,6 +94,28 @@ include 'views/layouts/header.php';
</div>
</div>
<div class="mb-4">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="is_short_story" name="is_short_story" value="1"
<?= (!empty($_POST['is_short_story']) && $_POST['is_short_story']) ? 'checked' : '' ?> onchange="toggleStoryEditor()">
<label class="form-check-label" for="is_short_story">
Это рассказ (без разбивки на главы)
</label>
</div>
</div>
<div class="card mb-4" id="story-editor-card" style="display: none;">
<div class="card-header">
<h5 class="card-title mb-0">Содержание рассказа</h5>
</div>
<div class="card-body">
<div id="story-editor" style="height: 400px;" data-content="<?= htmlspecialchars($_POST['content'] ?? '', ENT_QUOTES) ?>">
<?= $_POST['content'] ?? '' ?>
</div>
<input type="hidden" name="content" id="story-content" <?= !empty($_POST['content']) ? 'value="' . htmlspecialchars($_POST['content'], ENT_QUOTES) . '"' : '' ?>>
</div>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-journal-plus"></i> Создать книгу
@@ -107,6 +129,44 @@ include 'views/layouts/header.php';
</div>
</div>
</div>
</div>
<script src="<?= SITE_URL ?>/assets/js/editor.js"></script>
<script src="<?= SITE_URL ?>/assets/js/autosave.js"></script>
<script>
function toggleStoryEditor() {
var isShortStory = document.getElementById('is_short_story').checked;
var storyCard = document.getElementById('story-editor-card');
if (storyCard) {
storyCard.style.display = isShortStory ? 'block' : 'none';
if (isShortStory && !window.writerEditor) {
initQuillEditor(true);
} else if (!isShortStory && window.writerEditor) {
window.writerEditor.quill.destroy();
window.writerEditor = null;
}
if (isShortStory && window.writerEditor && window.writerEditor.quill) {
setTimeout(() => {
window.writerEditor.quill.root.style.height = '400px';
window.writerEditor.quill.container.style.height = '400px';
}, 100);
}
}
}
document.addEventListener('DOMContentLoaded', function() {
toggleStoryEditor();
window.addEventListener('resize', function() {
var isShortStory = document.getElementById('is_short_story').checked;
if (isShortStory && window.writerEditor && window.writerEditor.quill) {
window.writerEditor.quill.root.style.height = '400px';
window.writerEditor.quill.container.style.height = '400px';
}
});
});
</script>
<?php include 'views/layouts/footer.php'; ?>
+58 -145
View File
@@ -18,16 +18,19 @@ include 'views/layouts/header.php';
</div>
<?php endif; ?>
<div class="row">
<div class="col-lg-8">
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title mb-0">Основная информация</h5>
</div>
<div class="card-body">
<form method="post" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="<?= generate_csrf_token() ?>">
<form method="post" enctype="multipart/form-data" id="book-form">
<input type="hidden" name="csrf_token" value="<?= generate_csrf_token() ?>">
<!-- Аккордеон для основной информации (свернут) -->
<div class="accordion mb-4" id="bookInfoAccordion">
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#bookInfoCollapse">
<i class="bi bi-info-circle me-2"></i> Основная информация
</button>
</h2>
<div id="bookInfoCollapse" class="accordion-collapse collapse" data-bs-parent="#bookInfoAccordion">
<div class="accordion-body">
<div class="mb-3">
<label for="title" class="form-label">Название книги *</label>
<input type="text" class="form-control" id="title" name="title"
@@ -83,14 +86,38 @@ include 'views/layouts/header.php';
</div>
</div>
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-circle"></i> Сохранить изменения
</button>
</form>
<div class="mb-4">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="is_short_story" name="is_short_story" value="1"
<?= !empty($book['is_short_story']) ? 'checked' : '' ?> onchange="toggleStoryEditor()">
<label class="form-check-label" for="is_short_story">
Это рассказ (без разбивки на главы)
</label>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="card mb-4">
<!-- Редактор рассказа (всегда видимый) -->
<div class="mb-4" id="story-editor-card" style="display: <?= !empty($book['is_short_story']) ? 'block' : 'none' ?>">
<div class="card">
<div class="card-header bg-success bg-opacity-10">
<h5 class="card-title mb-0 text-success">
<i class="bi bi-book me-2"></i>Содержание рассказа
</h5>
</div>
<div class="card-body">
<div id="story-editor" style="height: 500px;" data-content="<?= htmlspecialchars($book['content'] ?? '', ENT_QUOTES) ?>"></div>
<textarea name="content" id="content" style="display: none;"><?= htmlspecialchars($book['content'] ?? '', ENT_QUOTES) ?></textarea>
</div>
</div>
</div>
<!-- Главы книги (всегда видимые) -->
<div class="mb-4" id="chapters-card" style="display: <?= !empty($book['is_short_story']) ? 'none' : 'block' ?>">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Главы книги</h5>
</div>
@@ -145,136 +172,22 @@ include 'views/layouts/header.php';
</div>
</div>
<div class="col-lg-4">
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title mb-0">Обложка книги</h5>
</div>
<div class="card-body">
<?php if (!empty($book['cover_image'])): ?>
<div class="text-center mb-3">
<img src="<?= COVERS_URL . e($book['cover_image']) ?>"
alt="Обложка"
class="img-fluid rounded"
style="max-height: 200px;">
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" name="delete_cover" value="1" id="delete_cover">
<label class="form-check-label" for="delete_cover">
Удалить обложку
</label>
</div>
<?php endif; ?>
<div class="mb-3">
<label for="cover_image" class="form-label">Загрузить новую обложку</label>
<input type="file" class="form-control" id="cover_image" name="cover_image"
accept="image/jpeg, image/png, image/gif, image/webp">
<div class="form-text">
Разрешены: JPG, PNG, GIF, WebP. Максимальный размер: 5MB.
</div>
</div>
<?php if (!empty($cover_error)): ?>
<div class="alert alert-danger">
<i class="bi bi-exclamation-triangle"></i> <?= e($cover_error) ?>
</div>
<?php endif; ?>
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title mb-0">Публичная ссылка</h5>
</div>
<div class="card-body">
<div class="input-group mb-2">
<input type="text"
id="share-link"
value="<?= e(SITE_URL . '/book/' . $book['share_token']) ?>"
readonly
class="form-control">
<button type="button" onclick="copyShareLink()" class="btn btn-outline-secondary">
<i class="bi bi-clipboard"></i>
</button>
</div>
<form method="post" action="<?= SITE_URL ?>/books/<?= $book['id'] ?>/regenerate-token" class="d-inline">
<input type="hidden" name="csrf_token" value="<?= generate_csrf_token() ?>">
<button type="submit" class="btn btn-outline-warning btn-sm" onclick="return confirm('Создать новую ссылку? Старая ссылка перестанет работать.')">
<i class="bi bi-arrow-repeat"></i> Обновить ссылку
</button>
</form>
<div class="form-text">
В публичном просмотре отображаются только опубликованные главы
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title mb-0">Экспорт книги</h5>
</div>
<div class="card-body">
<div class="d-grid gap-2">
<a href="<?= SITE_URL ?>/export/<?= $book['id'] ?>/pdf" class="btn btn-outline-danger" target="_blank">
<i class="bi bi-file-pdf"></i> PDF
</a>
<a href="<?= SITE_URL ?>/export/<?= $book['id'] ?>/docx" class="btn btn-outline-primary" target="_blank">
<i class="bi bi-file-word"></i> DOCX
</a>
<a href="<?= SITE_URL ?>/export/<?= $book['id'] ?>/html" class="btn btn-outline-success" target="_blank">
<i class="bi bi-file-code"></i> HTML
</a>
<a href="<?= SITE_URL ?>/export/<?= $book['id'] ?>/txt" class="btn btn-outline-secondary" target="_blank">
<i class="bi bi-file-text"></i> TXT
</a>
</div>
<div class="form-text mt-2">
Экспортируются все главы книги (включая черновики)
</div>
</div>
</div>
<div class="card border-danger">
<div class="card-header bg-danger text-white">
<h5 class="card-title mb-0">Опасная зона</h5>
</div>
<div class="card-body">
<p class="card-text small text-muted">
Удаление книги приведет к удалению всех глав и обложки. Это действие нельзя отменить.
</p>
<form method="post" action="<?= SITE_URL ?>/books/<?= $book['id'] ?>/delete"
onsubmit="return confirm('Вы уверены, что хотите удалить книгу «<?= e($book['title']) ?>»? Все главы также будут удалены.');">
<input type="hidden" name="csrf_token" value="<?= generate_csrf_token() ?>">
<button type="submit" class="btn btn-danger w-100">
<i class="bi bi-trash"></i> Удалить книгу
</button>
</form>
</div>
</div>
<!-- Кнопка сохранения -->
<div class="mb-4">
<button type="submit" class="btn btn-primary btn-lg">
<i class="bi bi-check-circle"></i> Сохранить изменения
</button>
</div>
</div>
</div>
</form>
<script>
function copyShareLink() {
const shareLink = document.getElementById('share-link');
shareLink.select();
document.execCommand('copy');
// Показать уведомление
const button = event.target;
const originalHTML = button.innerHTML;
button.innerHTML = '<i class="bi bi-check"></i>';
button.classList.remove('btn-outline-secondary');
button.classList.add('btn-success');
setTimeout(() => {
button.innerHTML = originalHTML;
button.classList.remove('btn-success');
button.classList.add('btn-outline-secondary');
}, 2000);
}
</script>
<script src="<?= SITE_URL ?>/assets/js/editor.js"></script>
<?php include 'views/layouts/footer.php'; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
const checkbox = document.getElementById('is_short_story');
if (checkbox && checkbox.checked) {
toggleStoryEditor();
}
});
</script>
</div>
+40 -21
View File
@@ -49,6 +49,11 @@ include 'views/layouts/header.php';
<div class="flex-grow-1">
<div class="d-flex justify-content-between align-items-start">
<h5 class="card-title mb-1">
<?php if (!empty($book['is_short_story'])): ?>
<i class="bi bi-file-text text-success me-1"></i>
<?php else: ?>
<i class="bi bi-book text-primary me-1"></i>
<?php endif; ?>
<a href="<?= SITE_URL ?>/books/<?= $book['id'] ?>/edit" class="text-decoration-none">
<?= e($book['title']) ?>
</a>
@@ -70,26 +75,38 @@ include 'views/layouts/header.php';
<?php endif; ?>
<div class="row text-center mb-3">
<div class="col-4">
<div class="border-end">
<div class="fw-bold text-primary"><?= $book['chapter_count'] ?? 0 ?></div>
<small class="text-muted">глав</small>
</div>
</div>
<div class="col-4">
<div class="border-end">
<div class="fw-bold text-success"><?= number_format($book['total_words'] ?? 0) ?></div>
<small class="text-muted">слов</small>
</div>
</div>
<div class="col-4">
<div>
<div class="fw-bold text-info">
<?= $book['chapter_count'] > 0 ? number_format(($book['total_words'] ?? 0) / $book['chapter_count']) : 0 ?>
<?php if (!empty($book['is_short_story'])): ?>
<div class="col-6">
<div class="border-end">
<div class="fw-bold text-success"><?= number_format($book['total_words'] ?? 0) ?></div>
<small class="text-muted">слов</small>
</div>
<small class="text-muted">слов/глава</small>
</div>
</div>
<div class="col-6">
<small class="text-success">(рассказ)</small>
</div>
<?php else: ?>
<div class="col-4">
<div class="border-end">
<div class="fw-bold text-primary"><?= $book['chapter_count'] ?? 0 ?></div>
<small class="text-muted">глав</small>
</div>
</div>
<div class="col-4">
<div class="border-end">
<div class="fw-bold text-success"><?= number_format($book['total_words'] ?? 0) ?></div>
<small class="text-muted">слов</small>
</div>
</div>
<div class="col-4">
<div>
<div class="fw-bold text-info">
<?= $book['chapter_count'] > 0 ? number_format(($book['total_words'] ?? 0) / $book['chapter_count']) : 0 ?>
</div>
<small class="text-muted">слов/глава</small>
</div>
</div>
<?php endif; ?>
</div>
<div class="d-grid gap-2">
@@ -97,9 +114,11 @@ include 'views/layouts/header.php';
<i class="bi bi-pencil"></i> Редактировать
</a>
<div class="btn-group" role="group">
<a href="<?= SITE_URL ?>/books/<?= $book['id'] ?>/chapters" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-file-text"></i> Главы
</a>
<?php if (empty($book['is_short_story'])): ?>
<a href="<?= SITE_URL ?>/books/<?= $book['id'] ?>/chapters" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-file-text"></i> Главы
</a>
<?php endif; ?>
<a href="<?= SITE_URL ?>/book/<?= $book['share_token'] ?>" class="btn btn-outline-success btn-sm" target="_blank">
<i class="bi bi-eye"></i> Просмотр
</a>
+22 -5
View File
@@ -40,12 +40,22 @@ include 'views/layouts/header.php';
<?php endif; ?>
<div class="d-flex justify-content-center gap-4 flex-wrap mb-4">
<?php if (!$is_short_story): ?>
<div class="text-center">
<div class="h4 text-primary mb-0"><?= count($chapters) ?></div>
<small class="text-muted">Глав</small>
</div>
<?php endif; ?>
<div class="text-center">
<div class="h4 text-success mb-0"><?= array_sum(array_column($chapters, 'word_count')) ?></div>
<div class="h4 text-success mb-0">
<?php if ($is_short_story && !empty($book['content'])): ?>
<?= number_format(mb_strlen($book['content']) - mb_strlen(str_replace(' ', '', $book['content'])) + 1) ?>
<?php elseif (!$is_short_story): ?>
<?= array_sum(array_column($chapters, 'word_count')) ?>
<?php else: ?>
0
<?php endif; ?>
</div>
<small class="text-muted">Слов</small>
</div>
</div>
@@ -81,7 +91,13 @@ include 'views/layouts/header.php';
</div>
</header>
<?php if (empty($chapters)): ?>
<?php if ($is_short_story && !empty($book['content'])): ?>
<div class="chapter-content mb-5">
<div class="chapter-text" style="line-height: 1.8; font-size: 1.1em;">
<?= $book['content'] ?>
</div>
</div>
<?php elseif (empty($chapters)): ?>
<div class="text-center py-5">
<i class="bi bi-file-text fs-1 text-muted"></i>
<h3 class="h4 text-muted mt-3">В этой книге пока нет глав</h3>
@@ -105,9 +121,9 @@ include 'views/layouts/header.php';
<?php endforeach; ?>
</div>
</div>
<hr class="my-5">
<?php foreach ($chapters as $index => $chapter): ?>
<div class="chapter-content mb-5" id="chapter-<?= $chapter['id'] ?>">
<h2 class="border-bottom pb-2 mb-4">
@@ -117,7 +133,7 @@ include 'views/layouts/header.php';
<div class="chapter-text" style="line-height: 1.8; font-size: 1.1em;">
<?= $chapter['content'] ?>
</div>
<div class="text-center mt-4">
<a href="#top" class="btn btn-outline-primary btn-sm">
<i class="bi bi-arrow-up"></i> Наверх
@@ -146,6 +162,7 @@ include 'views/layouts/header.php';
.chapter-text p {
margin-bottom: 1.5em;
text-align: justify;
text-indent: 2em;
}
.chapter-text .dialogue {
+1
View File
@@ -44,6 +44,7 @@
.chapter-content p {
margin-bottom: 1em;
text-align: justify;
text-indent: 2em;
}
.chapter-content code {
background: var(--bs-light);
+10 -5
View File
@@ -117,16 +117,21 @@ include 'views/layouts/header.php';
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">
Глав: <?= $book['chapter_count'] ?? 0 ?> |
Слов: <?= $book['total_words'] ?? 0 ?>
<?php if (!empty($book['is_short_story'])): ?>
(рассказ) | <?= number_format($book['total_words'] ?? 0) ?> слов
<?php else: ?>
Глав: <?= $book['chapter_count'] ?? 0 ?> | <?= number_format($book['total_words'] ?? 0) ?> слов
<?php endif; ?>
</small>
<div class="btn-group btn-group-sm">
<a href="<?= SITE_URL ?>/books/<?= $book['id'] ?>/edit" class="btn btn-outline-primary btn-sm">
<i class="bi bi-pencil"></i>
</a>
<a href="<?= SITE_URL ?>/books/<?= $book['id'] ?>/chapters" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-file-text"></i>
</a>
<?php if (empty($book['is_short_story'])): ?>
<a href="<?= SITE_URL ?>/books/<?= $book['id'] ?>/chapters" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-file-text"></i>
</a>
<?php endif; ?>
</div>
</div>
</div>
+2 -1
View File
@@ -22,8 +22,9 @@
</div>
</div>
</footer>
<script src="<?= SITE_URL ?>/assets/js/quill.js"></script>
<?php if (!isset($GLOBALS['bootstrap_loaded'])): ?>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<?php $GLOBALS['bootstrap_loaded'] = true; endif; ?>
<script>
// Функция для установки темы
function setTheme(themeName) {
+3
View File
@@ -49,6 +49,9 @@ $available_themes = [
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
<link rel="stylesheet" href="<?= SITE_URL ?>/assets/css/style.css">
<link href="<?= SITE_URL ?>/assets/css/quill.snow.css" rel="stylesheet">
<script src="<?= SITE_URL ?>/assets/js/quill.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<?php $GLOBALS['bootstrap_loaded'] = true; ?>
<style>
.navbar-brand { font-weight: 600; }
.dropdown-menu { min-width: 200px; }
+14 -3
View File
@@ -69,12 +69,20 @@ include 'views/layouts/header.php';
<div class="col-12">
<?php endif; ?>
<div class="d-flex justify-content-between align-items-start mb-2">
<h3 class="h4">
<h3 class="h4 mb-0">
<?php if ($book['sort_order_in_series']): ?>
<small class="text-muted">Книга <?= $book['sort_order_in_series'] ?></small><br>
<?php endif; ?>
<?php if (!empty($book['is_short_story'])): ?>
<i class="bi bi-file-text text-success me-1"></i>
<?php else: ?>
<i class="bi bi-book text-primary me-1"></i>
<?php endif; ?>
<?= e($book['title']) ?>
</h3>
<?php if (!empty($book['is_short_story'])): ?>
<span class="text-success">(рассказ)</span>
<?php endif; ?>
</div>
<?php if ($book['genre']): ?>
@@ -96,8 +104,11 @@ include 'views/layouts/header.php';
$book_stats = $book_model->getBookStats($book['id'], true);
?>
<small class="text-muted">
Глав: <?= $book_stats['chapter_count'] ?? 0 ?> |
Слов: <?= $book_stats['total_words'] ?? 0 ?>
<?php if (!empty($book_stats['is_short_story'])): ?>
<?= number_format($book_stats['total_words'] ?? 0) ?> слов
<?php else: ?>
Глав: <?= $book_stats['chapter_count'] ?? 0 ?> | <?= number_format($book_stats['total_words'] ?? 0) ?> слов
<?php endif; ?>
</small>
</div>
</div>
+18 -3
View File
@@ -81,7 +81,14 @@ include 'views/layouts/header.php';
</div>
<?php endif; ?>
<div class="flex-grow-1">
<h5 class="card-title"><?= e($book['title']) ?></h5>
<h5 class="card-title mb-0">
<?php if (!empty($book['is_short_story'])): ?>
<i class="bi bi-file-text text-success me-1"></i>
<?php else: ?>
<i class="bi bi-book text-primary me-1"></i>
<?php endif; ?>
<?= e($book['title']) ?>
</h5>
<?php if ($book['genre']): ?>
<p class="text-muted small mb-1"><em><?= e($book['genre']) ?></em></p>
<?php endif; ?>
@@ -102,10 +109,18 @@ include 'views/layouts/header.php';
<div class="d-flex justify-content-between align-items-center">
<a href="<?= SITE_URL ?>/book/<?= e($book['share_token']) ?>" class="btn btn-primary btn-sm">
<i class="bi bi-book"></i> Читать книгу
<?php if (!empty($book['is_short_story'])): ?>
<i class="bi bi-file-text"></i> Читать рассказ
<?php else: ?>
<i class="bi bi-book"></i> Читать книгу
<?php endif; ?>
</a>
<small class="text-muted">
Глав: <?= $chapter_count ?> | Слов: <?= $word_count ?>
<?php if (!empty($book['is_short_story'])): ?>
(рассказ) <?= number_format($word_count) ?> слов
<?php else: ?>
Глав: <?= $chapter_count ?> | <?= number_format($word_count) ?> слов
<?php endif; ?>
</small>
</div>
</div>