= e($author['display_name'] ?: $author['username']) ?>
- - - -Публикации автора
- - -У этого автора пока нет опубликованных книг
-Следите за обновлениями, скоро здесь появятся новые произведения!
-diff --git a/README.md b/README.md index d6e5a61..d9fb3ee 100644 --- a/README.md +++ b/README.md @@ -1,88 +1,88 @@ -# Web Writer - -**Лицензия:** AGPLv3 - -**Web Writer** — веб-приложение для написания, хранения и публикации книг и серий книг. Поддерживает Markdown с расширением для диалогов, автосохранение текста, экспорт и управление пользователями. - ---- - -## 🚀 Возможности - -- **Книги и серии:** создавайте серии и добавляйте книги с главами. -- **Редактор книг:** Markdown, автосохранение текста, интерактивное содержание. -- **Предпросмотр книг:** - - **Автор:** видит все черновики и опубликованные главы. - - **Публичный доступ:** только опубликованные главы по ссылке с `shared_token`. -- **Обложки и аватары:** добавляйте изображения к книгам и профилям. -- **Экспорт:** PDF, DOCX, HTML, TXT. -- **Администрирование пользователей:** - - Управление аккаунтами, активация/деактивация. - - При удалении пользователя удаляются все его книги. -- **Публичные ссылки:** делитесь `shared_token` для просмотра опубликованных глав. - ---- - -## ⚙️ Требования - -- **PHP:** 8.0 и выше -- **MySQL** с InnoDB и внешними ключами -- **PHP расширения:** `mbstring`, `json`, `PDO` -- Веб-сервер с правами на запись в папки `config/` и `uploads/` - -> Все библиотеки уже включены в `vendor/`. Composer не нужен. - ---- - -## 🛠 Установка - -1. Скопируйте файлы на веб-сервер. -2. Проверьте доступность папок `config/` и `uploads/` для записи. -3. Перейдите в браузере на `install.php` и следуйте шагам: - - **Шаг 1: Настройки базы данных** - - Хост БД - - Имя базы данных - - Пользователь и пароль - - **Шаг 2: Создание администратора** - - Имя пользователя - - Пароль - - Email (по желанию) - - Отображаемое имя (по желанию) - -4. После успешной установки файл `config/config.php` будет сгенерирован автоматически. -5. Перейдите на главную страницу приложения (`index.php`) и войдите под админом. -6. **Не забудьте удалить или переместить файл install.php!!!** - ---- - -## 📝 Конфигурация - -Файл `config/config.php` содержит: - -- Подключение к базе данных: `DB_HOST`, `DB_USER`, `DB_PASS`, `DB_NAME` -- Пути к файлам: - - `UPLOAD_PATH` — корневая папка загрузок - - `COVERS_PATH` / `COVERS_URL` — обложки книг - - `AVATARS_PATH` / `AVATARS_URL` — аватары пользователей -- Адрес сайта: `SITE_URL` -- Имя приложения: `APP_NAME` = "Web Writer" - ---- - -## 🛠 Дальнейшее развитие - -- Планирую вынести работу с сущностями (книги, главы, серии, пользователи) в контроллеры. -- Создать единую точку входа для приложения. - ---- - -## ❗ Поддержка - -Все ошибки и предложения шлите в issue - ---- - -## 📜 Лицензия - +# Web Writer + +**Лицензия:** AGPLv3 + +**Web Writer** — веб-приложение для написания, хранения и публикации книг и серий книг. Поддерживает Markdown с расширением для диалогов, автосохранение текста, экспорт и управление пользователями. + +--- + +## 🚀 Возможности + +- **Книги и серии:** создавайте серии и добавляйте книги с главами. +- **Редактор книг:** Markdown, автосохранение текста, интерактивное содержание. +- **Предпросмотр книг:** + - **Автор:** видит все черновики и опубликованные главы. + - **Публичный доступ:** только опубликованные главы по ссылке с `shared_token`. +- **Обложки и аватары:** добавляйте изображения к книгам и профилям. +- **Экспорт:** PDF, DOCX, HTML, TXT. +- **Администрирование пользователей:** + - Управление аккаунтами, активация/деактивация. + - При удалении пользователя удаляются все его книги. +- **Публичные ссылки:** делитесь `shared_token` для просмотра опубликованных глав. + +--- + +## ⚙️ Требования + +- **PHP:** 8.0 и выше +- **MySQL** с InnoDB и внешними ключами +- **PHP расширения:** `mbstring`, `json`, `PDO` +- Веб-сервер с правами на запись в папки `config/` и `uploads/` + +> Все библиотеки уже включены в `vendor/`. Composer не нужен. + +--- + +## 🛠 Установка + +1. Скопируйте файлы на веб-сервер. +2. Проверьте доступность папок `config/` и `uploads/` для записи. +3. Перейдите в браузере на `install.php` и следуйте шагам: + + **Шаг 1: Настройки базы данных** + - Хост БД + - Имя базы данных + - Пользователь и пароль + + **Шаг 2: Создание администратора** + - Имя пользователя + - Пароль + - Email (по желанию) + - Отображаемое имя (по желанию) + +4. После успешной установки файл `config/config.php` будет сгенерирован автоматически. +5. Перейдите на главную страницу приложения (`index.php`) и войдите под админом. +6. **Не забудьте удалить или переместить файл install.php!!!** + +--- + +## 📝 Конфигурация + +Файл `config/config.php` содержит: + +- Подключение к базе данных: `DB_HOST`, `DB_USER`, `DB_PASS`, `DB_NAME` +- Пути к файлам: + - `UPLOAD_PATH` — корневая папка загрузок + - `COVERS_PATH` / `COVERS_URL` — обложки книг + - `AVATARS_PATH` / `AVATARS_URL` — аватары пользователей +- Адрес сайта: `SITE_URL` +- Имя приложения: `APP_NAME` = "Web Writer" + +--- + +## 🛠 Дальнейшее развитие + +- Планирую вынести работу с сущностями (книги, главы, серии, пользователи) в контроллеры. +- Создать единую точку входа для приложения. + +--- + +## ❗ Поддержка + +Все ошибки и предложения шлите в issue + +--- + +## 📜 Лицензия + Приложение распространяется под лицензией [AGPLv3](https://www.gnu.org/licenses/agpl-3.0.html). \ No newline at end of file diff --git a/assets/css/style.css b/assets/css/style.css index 32377e2..8f53fb0 100755 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -1,925 +1,959 @@ -/* Базовые стили */ -h1, h2, h3, h4, h5, h6 { - margin-bottom: 1rem; -} - -article, textarea, main.container { - margin-top: 1rem; - margin-bottom: 1rem; - padding-top: 1rem; - padding-bottom: 1rem; -} - -article > header, article > footer { - margin-top: 0.1rem; - margin-bottom: 0.1rem; - padding-top: 0.2rem; - padding-bottom: 0.2rem; -} - -input:not([type="checkbox"], [type="radio"]), select { - padding: 4px; - height: 2em; -} - -/* Убираем конфликтующие стили Pico CSS */ -article header, article footer { - margin: 0; - padding: 0; -} - -article > header, article > footer { - margin: 0; - padding: 0; -} - -/* Уведомления */ -.alert { - padding: 1rem; - margin: 1rem 0; - border-radius: 5px; -} - -.alert-error { - background: #ffebee; - color: #c62828; - border: 1px solid #ffcdd2; -} - -.alert-success { - background: #e8f5e8; - color: #2e7d32; - border: 1px solid #c8e6c9; -} - -/* Кнопки */ -.compact-button { - padding: 3px 8px !important; - font-size: 0.85rem; - text-decoration: none; - display: inline-flex !important; - align-items: center; - justify-content: center; - border: 1px solid var(--secondary); - border-radius: 4px; - background: var(--secondary); - color: var(--secondary-inverse); - cursor: pointer; - height: 28px !important; - box-sizing: border-box; - line-height: 1 !important; - vertical-align: middle; -} - -.compact-button:hover { - opacity: 0.9; -} - -.button-group { - display: flex; - gap: 5px; - margin-bottom: 1rem; -} - -.button-group button, -.button-group a[role="button"] { - flex: 1; - padding: 0.5rem; - height: 38px; - display: flex; - align-items: center; - justify-content: center; - text-decoration: none; - box-sizing: border-box; -} - -.button-group .delete-btn { - background: #ff4444 !important; - border-color: #ff4444 !important; - color: white !important; -} - -.green-btn { - background: #449944 !important; - border-color: #449944 !important; - color: white !important; -} - -.green-btn:hover { - background: #44bb44 !important; - border-color: #44bb44 !important; - color: white !important; -} - -.profile-buttons { - display: flex; - gap: 10px; - align-items: stretch; -} - -.profile-button { - flex: 1; - padding: 0.75rem; - display: flex; - align-items: center; - justify-content: center; - text-decoration: none; - border: 1px solid; - border-radius: 4px; - font-size: 1rem; - cursor: pointer; - box-sizing: border-box; - height: 62px; - min-height: 44px; -} - -.profile-button.primary { - background: var(--primary); - border-color: var(--primary); - color: var(--primary-inverse); -} - -.profile-button.secondary { - background: var(--secondary); - border-color: var(--secondary); - color: var(--secondary-inverse); -} - -.adaptive-button { - padding: 8px 12px !important; - font-size: 0.85rem; - text-decoration: none; - display: inline-flex; - align-items: center; - justify-content: center; - border: 1px solid var(--secondary); - border-radius: 4px; - background: var(--secondary); - color: var(--secondary-inverse); - cursor: pointer; - box-sizing: border-box; - text-align: center; - white-space: normal; - word-break: break-word; - min-height: 44px; - flex: 1; - min-width: 120px; - margin: 2px; -} - -.adaptive-button:hover { - opacity: 0.9; -} - -.primary.adaptive-button { - background: var(--primary); - border-color: var(--primary); - color: var(--primary-inverse); -} - -.action-button { - display: inline-flex; - align-items: center; - justify-content: center; - padding: 0.75rem 1.5rem; - font-size: 0.9rem; - text-decoration: none; - border: 1px solid; - border-radius: 4px; - cursor: pointer; - box-sizing: border-box; - height: 44px; - min-width: 140px; - white-space: nowrap; - transition: all 0.3s ease; - text-align: center; -} - -.action-button.primary { - background: #007bff; - border-color: #007bff; - color: #fff; -} - -.action-button.primary:hover { - opacity: 0.9; -} - -.action-button.delete { - margin-top: 1rem; - background: #ff4444; - border-color: #ff4444; - color: white; -} - -.action-button.delete:hover { - background: #dd3333; - border-color: #dd3333; - color: white; -} - -/* Таблицы */ -.compact-table { - width: 100%; - font-size: 0.9rem; - border-collapse: collapse; -} - -.compact-table th, -.compact-table td { - padding: 6px 8px; - border-bottom: 1px solid #eee; -} - -.compact-table th { - background: #f5f5f5; - font-weight: bold; -} - -/* Markdown редактор */ -#content { - transition: all 0.3s ease; - border: 1px solid #ddd; - border-radius: 4px; - padding: 12px; - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; - font-size: 14px; - line-height: 1.5; - min-height: 400px; - color: #111; - background-color: #fff; - resize: vertical; -} - -#content:focus { - border-color: #007bff; - box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25); - outline: none; - color: #111; - background-color: #fff; -} - -/* Кастомный скроллбар для редактора */ -#content::-webkit-scrollbar { - width: 8px; -} - -#content::-webkit-scrollbar-track { - background: #f1f1f1; - border-radius: 4px; -} - -#content::-webkit-scrollbar-thumb { - background: #c1c1c1; - border-radius: 4px; -} - -#content::-webkit-scrollbar-thumb:hover { - background: #a8a8a8; -} - -#content { - scrollbar-width: thin; - scrollbar-color: #c1c1c1 #f1f1f1; -} - -/* Элементы управления редактором */ -.editor-controls { - position: sticky !important; - top: 10px !important; - right: 10px !important; - z-index: 100 !important; - display: flex; - gap: 5px; - justify-content: flex-end; - margin-bottom: 10px; -} - -.editor-controls button { - width: 40px !important; - height: 40px !important; - border-radius: 50% !important; - border: 1px solid #ddd !important; - background: white !important; - cursor: pointer !important; - font-size: 16px !important; - display: flex !important; - align-items: center !important; - justify-content: center !important; - box-shadow: 0 2px 5px rgba(0,0,0,0.2) !important; - transition: all 0.3s ease !important; - color: #333333 !important; -} - -.editor-controls button:hover { - transform: scale(1.1) !important; - background-color: #f8f9fa !important; - box-shadow: 0 4px 8px rgba(0,0,0,0.3) !important; -} - -/* Полноэкранный режим редактора */ -#fullscreen-controls { - position: fixed !important; - z-index: 9999 !important; - display: flex !important; - gap: 5px !important; -} - -#fullscreen-controls button { - width: 45px !important; - height: 45px !important; - border-radius: 50% !important; - border: 1px solid #ddd !important; - background: white !important; - cursor: pointer !important; - font-size: 18px !important; - display: flex !important; - align-items: center !important; - justify-content: center !important; - box-shadow: 0 2px 5px rgba(0,0,0,0.3) !important; - transition: all 0.3s ease !important; - color: #333333 !important; -} - -#fullscreen-controls button:hover { - transform: scale(1.1) !important; - background-color: #f8f9fa !important; - box-shadow: 0 4px 8px rgba(0,0,0,0.4) !important; -} - -/* Стили для отображения контента книг */ -.book-content { - line-height: 1.7; - font-family: Georgia, serif; -} - -.book-content h1 { - font-size: 2em; - margin-top: 2rem; - margin-bottom: 1rem; - border-bottom: 2px solid #eee; - padding-bottom: 0.5rem; -} - -.book-content h2 { - font-size: 1.6em; - margin-top: 1.5rem; - margin-bottom: 1rem; - border-bottom: 1px solid #eee; - padding-bottom: 0.3rem; -} - -.book-content h3 { - font-size: 1.3em; - margin-top: 1.2rem; - margin-bottom: 0.8rem; -} - -.book-content p { - margin-bottom: 1rem; - text-align: justify; -} - -.book-content blockquote { - border-left: 4px solid #007bff; - padding-left: 1.5rem; - margin-left: 0; - margin-right: 0; - color: #555; - font-style: italic; - background: #f8f9fa; - padding: 1rem; - border-radius: 0 5px 5px 0; -} - -.book-content code { - background: #f5f5f5; - padding: 2px 6px; - border-radius: 3px; - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; - font-size: 0.9em; -} - -.book-content pre { - background: #2d2d2d; - color: #f8f8f2; - padding: 1rem; - border-radius: 5px; - overflow-x: auto; - border-left: 4px solid #007bff; -} - -.book-content pre code { - background: none; - padding: 0; - color: inherit; -} - -.book-content ul, .book-content ol { - margin-bottom: 1rem; - padding-left: 2rem; -} - -.book-content li { - margin-bottom: 0.3rem; -} - -.book-content table { - width: 100%; - border-collapse: collapse; - margin-bottom: 1rem; - box-shadow: 0 1px 3px rgba(0,0,0,0.1); -} - -.book-content th, .book-content td { - border: 1px solid #ddd; - padding: 10px 12px; - text-align: left; -} - -.book-content th { - background: #f5f5f5; - font-weight: bold; -} - -.book-content tr:nth-child(even) { - background: #f9f9f9; -} - -.dialogue { - margin-left: 2rem; - font-style: italic; - color: #2c5aa0; -} - -/* Обложки и медиа */ -.book-cover { - transition: transform 0.3s ease; -} - -.book-cover:hover { - transform: scale(1.05); -} - -.cover-placeholder { - width: 120px; - height: 160px; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - border-radius: 4px; - display: flex; - align-items: center; - justify-content: center; - color: white; - font-size: 2rem; - margin: 0 auto 1rem; -} - -/* Аватарки и профиль */ -.avatar-container { - text-align: center; - margin-bottom: 1.5rem; -} - -.avatar { - width: 150px; - height: 150px; - border-radius: 50%; - border: 3px solid #007bff; - object-fit: cover; -} - -.avatar-placeholder { - 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; -} - -.author-bio { - background: #f8f9fa; - padding: 1.5rem; - border-radius: 8px; - margin: 1rem 0; - line-height: 1.6; -} - -.author-bio h1, .author-bio h2, .author-bio h3 { - margin-top: 1rem; - margin-bottom: 0.5rem; -} - -.author-bio p { - margin-bottom: 1rem; -} - -.author-bio ul, .author-bio ol { - margin-bottom: 1rem; - padding-left: 2rem; -} - -.author-bio blockquote { - border-left: 4px solid #007bff; - padding-left: 1rem; - margin-left: 0; - color: #555; - font-style: italic; -} - -/* Статистика */ -.author-stats { - display: flex; - justify-content: center; - gap: 2rem; - flex-wrap: wrap; - margin: 1rem 0; -} - -.stat-item { - text-align: center; -} - -.stat-number { - font-size: 1.5em; - font-weight: bold; - color: #007bff; -} - -.stat-label { - font-size: 0.9em; - color: #666; -} - -/* Серии книг */ -.series-books article { - transition: transform 0.2s ease, box-shadow 0.2s ease; - border: 1px solid #e0e0e0; -} - -.series-books article:hover { - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0,0,0,0.1); -} - -.series-info { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - color: white; - padding: 1rem; - border-radius: 8px; - margin-bottom: 2rem; -} - -.series-badge { - display: inline-block; - background: #007bff; - color: white; - padding: 0.2rem 0.5rem; - border-radius: 12px; - font-size: 0.8rem; - margin-left: 0.5rem; -} - -/* Dashboard */ -.dashboard-buttons { - display: flex; - gap: 10px; - margin-top: 1rem; - flex-wrap: nowrap; -} - -.dashboard-button { - flex: 1; - text-align: center; - padding: 0.75rem 0.5rem; - text-decoration: none; - border: 1px solid var(--secondary); - border-radius: 4px; - background: var(--secondary); - color: var(--secondary-inverse); - font-size: 0.9rem; - transition: all 0.3s ease; - min-height: 44px; - display: flex; - align-items: center; - justify-content: center; - white-space: nowrap; -} - -.dashboard-button:hover { - opacity: 0.9; - transform: translateY(-1px); -} - -.dashboard-button.new { - background: var(--primary); - border-color: var(--primary); - color: var(--primary-inverse); - flex: 0.7; -} - -.stats-list { - margin-top: 1rem; -} - -.stats-list p { - margin: 0.5rem 0; - padding: 0.3rem 0; - border-bottom: 1px solid #f0f0f0; -} - -.stats-list p:last-child { - border-bottom: none; -} - -.series-stats { - margin-top: 1rem; - padding: 1rem; - background: #f8f9fa; - border-radius: 5px; - border-left: 4px solid #6f42c1; -} - -.series-stats p { - margin: 0.5rem 0; - font-size: 0.9rem; -} - -.dashboard-section { - margin-top: 2rem; - padding-top: 1rem; - border-top: 1px solid #eee; -} - -.dashboard-item { - transition: transform 0.2s ease, box-shadow 0.2s ease; - border: 1px solid #e0e0e0; - padding: 1rem; -} - -.dashboard-item:hover { - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0,0,0,0.1); -} - -.welcome-message { - text-align: center; - padding: 3rem; - background: #f9f9f9; - border-radius: 8px; - margin-top: 2rem; -} - -.welcome-buttons { - display: flex; - gap: 1rem; - justify-content: center; - flex-wrap: wrap; - margin-top: 1.5rem; -} - -.action-buttons { - display: flex; - gap: 5px; - flex-wrap: wrap; - margin-top: 0.5rem; -} - -.action-buttons .compact-button { - flex: 1; - min-width: 80px; - text-align: center; - font-size: 0.8rem; - padding: 0.3rem 0.5rem; -} - -/* Улучшения для grid в dashboard */ -.grid { - gap: 1rem; -} - -.grid article { - margin: 0; - padding: 1.5rem; -} - -/* Адаптивность */ -@media (max-width: 768px) { - .adaptive-button { - flex: 1 1 calc(50% - 10px); - min-width: calc(50% - 10px); - font-size: 0.8rem; - padding: 10px 8px !important; - } - - .action-button { - padding: 0.6rem 1rem; - font-size: 0.85rem; - min-width: 120px; - height: 42px; - } - - .book-content { - font-size: 16px; - line-height: 1.6; - } - - .book-content h1 { - font-size: 1.6em; - } - - .book-content h2 { - font-size: 1.4em; - } - - .book-content h3 { - font-size: 1.2em; - } - - .book-content pre { - font-size: 14px; - } - - .author-books article { - flex-direction: column; - } - - .author-books .book-cover { - align-self: center; - } - - .avatar, .avatar-placeholder { - width: 120px; - height: 120px; - font-size: 2.5rem; - } - - .author-stats { - gap: 1rem; - } - - .stat-number { - font-size: 1.3em; - } - - .dashboard-buttons { - flex-direction: column; - gap: 8px; - } - - .dashboard-button { - flex: none; - width: 100%; - } - - .dashboard-button.new { - flex: none; - width: 100%; - } - - .welcome-buttons { - flex-direction: column; - align-items: center; - } - - .welcome-buttons a { - width: 100%; - max-width: 250px; - } - - .action-buttons { - flex-direction: column; - } - - .action-buttons .compact-button { - width: 100%; - } - - #fullscreen-controls { - top: 10px !important; - right: 10px !important; - } - - #fullscreen-controls button { - width: 60px !important; - height: 60px !important; - font-size: 24px !important; - border: 2px solid #ddd !important; - } - - .editor-controls button { - width: 50px !important; - height: 50px !important; - font-size: 20px !important; - } -} - -@media (max-width: 480px) { - .adaptive-button { - flex: 1 1 100%; - min-width: 100%; - } - - .action-button { - padding: 0.5rem 0.8rem; - font-size: 0.8rem; - min-width: 110px; - height: 40px; - } - - .action-buttons-container { - flex-direction: column; - width: 100%; - } - - .action-buttons-container .action-button { - width: 100%; - min-width: auto; - } - - .avatar, .avatar-placeholder { - width: 100px; - height: 100px; - font-size: 2rem; - } - - .author-stats { - flex-direction: column; - gap: 0.5rem; - } - - .dashboard-item { - padding: 0.8rem; - } - - .dashboard-button { - font-size: 0.85rem; - padding: 0.6rem 0.4rem; - } - - .welcome-message { - padding: 2rem 1rem; - } - - #fullscreen-controls button { - width: 55px !important; - height: 55px !important; - font-size: 22px !important; - } -} - -@media (min-width: 769px) { - #fullscreen-controls { - top: 15px !important; - right: 15px !important; - } - - #fullscreen-controls button { - width: 50px !important; - height: 50px !important; - font-size: 20px !important; - } -} - -/* Полноэкранные режимы редактора */ -#content.mobile-fullscreen { - position: fixed !important; - top: 50px !important; - left: 0 !important; - width: 100vw !important; - height: calc(100vh - 100px) !important; - z-index: 9998 !important; - background-color: white !important; - border: 2px solid #007bff !important; - border-radius: 0 !important; - font-size: 18px !important; - padding: 15px !important; - margin: 0 !important; - box-sizing: border-box !important; - resize: none !important; - box-shadow: none !important; - overflow-y: auto !important; - -webkit-overflow-scrolling: touch !important; -} - -#content.desktop-fullscreen { - position: fixed !important; - top: 5vh !important; - left: 5vw !important; - width: 90vw !important; - height: 90vh !important; - z-index: 9998 !important; - background-color: white !important; - border: 2px solid #007bff !important; - border-radius: 8px !important; - font-size: 16px !important; - padding: 20px !important; - margin: 0 !important; - box-sizing: border-box !important; - resize: none !important; - box-shadow: 0 0 20px rgba(0,0,0,0.3) !important; +/* Базовые стили */ +h1, h2, h3, h4, h5, h6 { + margin-bottom: 1rem; +} + +article, textarea, main.container { + margin-top: 1rem; + margin-bottom: 1rem; + padding-top: 1rem; + padding-bottom: 1rem; +} + +article > header, article > footer { + margin-top: 0.1rem; + margin-bottom: 0.1rem; + padding-top: 0.2rem; + padding-bottom: 0.2rem; +} + +input:not([type="checkbox"], [type="radio"]), select { + padding: 4px; + height: 2em; +} + +/* Убираем конфликтующие стили Pico CSS */ +article header, article footer { + margin: 0; + padding: 0; +} + +article > header, article > footer { + margin: 0; + padding: 0; +} + +/* Уведомления */ +.alert { + padding: 1rem; + margin: 1rem 0; + border-radius: 5px; +} + +.alert-error { + background: #ffebee; + color: #c62828; + border: 1px solid #ffcdd2; +} + +.alert-success { + background: #e8f5e8; + color: #2e7d32; + border: 1px solid #c8e6c9; +} + +/* Кнопки */ +.compact-button { + padding: 3px 8px !important; + font-size: 0.85rem; + text-decoration: none; + display: inline-flex !important; + align-items: center; + justify-content: center; + border: 1px solid var(--secondary); + border-radius: 4px; + background: var(--secondary); + color: var(--secondary-inverse); + cursor: pointer; + height: 28px !important; + box-sizing: border-box; + line-height: 1 !important; + vertical-align: middle; +} + +.compact-button:hover { + opacity: 0.9; +} + +.button-group { + display: flex; + gap: 5px; + margin-bottom: 1rem; +} + +.button-group button, +.button-group a[role="button"] { + flex: 1; + padding: 0.5rem; + height: 38px; + display: flex; + align-items: center; + justify-content: center; + text-decoration: none; + box-sizing: border-box; +} + +.button-group .delete-btn { + background: #ff4444 !important; + border-color: #ff4444 !important; + color: white !important; +} + +.green-btn { + background: #449944 !important; + border-color: #449944 !important; + color: white !important; +} + +.green-btn:hover { + background: #44bb44 !important; + border-color: #44bb44 !important; + color: white !important; +} + +.profile-buttons { + display: flex; + gap: 10px; + align-items: stretch; +} + +.profile-button { + flex: 1; + padding: 0.75rem; + display: flex; + align-items: center; + justify-content: center; + text-decoration: none; + border: 1px solid; + border-radius: 4px; + font-size: 1rem; + cursor: pointer; + box-sizing: border-box; + height: 62px; + min-height: 44px; +} + +.profile-button.primary { + background: var(--primary); + border-color: var(--primary); + color: var(--primary-inverse); +} + +.profile-button.secondary { + background: var(--secondary); + border-color: var(--secondary); + color: var(--secondary-inverse); +} + +.adaptive-button { + padding: 8px 12px !important; + font-size: 0.85rem; + text-decoration: none; + display: inline-flex; + align-items: center; + justify-content: center; + border: 1px solid var(--secondary); + border-radius: 4px; + background: var(--secondary); + color: var(--secondary-inverse); + cursor: pointer; + box-sizing: border-box; + text-align: center; + white-space: normal; + word-break: break-word; + min-height: 44px; + flex: 1; + min-width: 120px; + margin: 2px; +} + +.adaptive-button:hover { + opacity: 0.9; +} + +.primary.adaptive-button { + background: var(--primary); + border-color: var(--primary); + color: var(--primary-inverse); +} + +.action-button { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.75rem 1.5rem; + font-size: 0.9rem; + text-decoration: none; + border: 1px solid; + border-radius: 4px; + cursor: pointer; + box-sizing: border-box; + height: 44px; + min-width: 140px; + white-space: nowrap; + transition: all 0.3s ease; + text-align: center; +} + +.action-button.primary { + background: #007bff; + border-color: #007bff; + color: #fff; +} + +.action-button.primary:hover { + opacity: 0.9; +} + +.action-button.delete { + margin-top: 1rem; + background: #ff4444; + border-color: #ff4444; + color: white; +} + +.action-button.delete:hover { + background: #dd3333; + border-color: #dd3333; + color: white; +} + +/* Таблицы */ +.compact-table { + width: 100%; + font-size: 0.9rem; + border-collapse: collapse; +} + +.compact-table th, +.compact-table td { + padding: 6px 8px; + border-bottom: 1px solid #eee; +} + +.compact-table th { + background: #f5f5f5; + font-weight: bold; +} + +/* Markdown редактор */ +#content { + transition: all 0.3s ease; + border: 1px solid #ddd; + border-radius: 4px; + padding: 12px; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 14px; + line-height: 1.5; + min-height: 400px; + color: #111; + background-color: #fff; + resize: vertical; +} + +#content:focus { + border-color: #007bff; + box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25); + outline: none; + color: #111; + background-color: #fff; +} + +/* Кастомный скроллбар для редактора */ +#content::-webkit-scrollbar { + width: 8px; +} + +#content::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 4px; +} + +#content::-webkit-scrollbar-thumb { + background: #c1c1c1; + border-radius: 4px; +} + +#content::-webkit-scrollbar-thumb:hover { + background: #a8a8a8; +} + +#content { + scrollbar-width: thin; + scrollbar-color: #c1c1c1 #f1f1f1; +} + +/* Элементы управления редактором */ +.editor-controls { + position: sticky !important; + top: 10px !important; + right: 10px !important; + z-index: 100 !important; + display: flex; + gap: 5px; + justify-content: flex-end; + margin-bottom: 10px; +} + +.editor-controls button { + width: 40px !important; + height: 40px !important; + border-radius: 50% !important; + border: 1px solid #ddd !important; + background: white !important; + cursor: pointer !important; + font-size: 16px !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + box-shadow: 0 2px 5px rgba(0,0,0,0.2) !important; + transition: all 0.3s ease !important; + color: #333333 !important; +} + +.editor-controls button:hover { + transform: scale(1.1) !important; + background-color: #f8f9fa !important; + box-shadow: 0 4px 8px rgba(0,0,0,0.3) !important; +} + +/* Полноэкранный режим редактора */ +#fullscreen-controls { + position: fixed !important; + z-index: 9999 !important; + display: flex !important; + gap: 5px !important; +} + +#fullscreen-controls button { + width: 45px !important; + height: 45px !important; + border-radius: 50% !important; + border: 1px solid #ddd !important; + background: white !important; + cursor: pointer !important; + font-size: 18px !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + box-shadow: 0 2px 5px rgba(0,0,0,0.3) !important; + transition: all 0.3s ease !important; + color: #333333 !important; +} + +#fullscreen-controls button:hover { + transform: scale(1.1) !important; + background-color: #f8f9fa !important; + box-shadow: 0 4px 8px rgba(0,0,0,0.4) !important; +} + +/* Стили для отображения контента книг */ +.book-content { + line-height: 1.7; + font-family: Georgia, serif; +} + +.book-content h1 { + font-size: 2em; + margin-top: 2rem; + margin-bottom: 1rem; + border-bottom: 2px solid #eee; + padding-bottom: 0.5rem; +} + +.book-content h2 { + font-size: 1.6em; + margin-top: 1.5rem; + margin-bottom: 1rem; + border-bottom: 1px solid #eee; + padding-bottom: 0.3rem; +} + +.book-content h3 { + font-size: 1.3em; + margin-top: 1.2rem; + margin-bottom: 0.8rem; +} + +.book-content p { + margin-bottom: 1rem; + text-align: justify; +} + +.book-content blockquote { + border-left: 4px solid #007bff; + padding-left: 1.5rem; + margin-left: 0; + margin-right: 0; + color: #555; + font-style: italic; + background: #f8f9fa; + padding: 1rem; + border-radius: 0 5px 5px 0; +} + +.book-content code { + background: #f5f5f5; + padding: 2px 6px; + border-radius: 3px; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 0.9em; +} + +.book-content pre { + background: #2d2d2d; + color: #f8f8f2; + padding: 1rem; + border-radius: 5px; + overflow-x: auto; + border-left: 4px solid #007bff; +} + +.book-content pre code { + background: none; + padding: 0; + color: inherit; +} + +.book-content ul, .book-content ol { + margin-bottom: 1rem; + padding-left: 2rem; +} + +.book-content li { + margin-bottom: 0.3rem; +} + +.book-content table { + width: 100%; + border-collapse: collapse; + margin-bottom: 1rem; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); +} + +.book-content th, .book-content td { + border: 1px solid #ddd; + padding: 10px 12px; + text-align: left; +} + +.book-content th { + background: #f5f5f5; + font-weight: bold; +} + +.book-content tr:nth-child(even) { + background: #f9f9f9; +} + +.dialogue { + margin-left: 2rem; + font-style: italic; + color: #2c5aa0; +} + +/* Обложки и медиа */ +.book-cover { + transition: transform 0.3s ease; +} + +.book-cover:hover { + transform: scale(1.05); +} + +.cover-placeholder { + width: 120px; + height: 160px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 2rem; + margin: 0 auto 1rem; +} + +/* Аватарки и профиль */ +.avatar-container { + text-align: center; + margin-bottom: 1.5rem; +} + +.avatar { + width: 150px; + height: 150px; + border-radius: 50%; + border: 3px solid #007bff; + object-fit: cover; +} + +.avatar-placeholder { + 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; +} + +.author-bio { + background: #f8f9fa; + padding: 1.5rem; + border-radius: 8px; + margin: 1rem 0; + line-height: 1.6; +} + +.author-bio h1, .author-bio h2, .author-bio h3 { + margin-top: 1rem; + margin-bottom: 0.5rem; +} + +.author-bio p { + margin-bottom: 1rem; +} + +.author-bio ul, .author-bio ol { + margin-bottom: 1rem; + padding-left: 2rem; +} + +.author-bio blockquote { + border-left: 4px solid #007bff; + padding-left: 1rem; + margin-left: 0; + color: #555; + font-style: italic; +} + +/* Статистика */ +.author-stats { + display: flex; + justify-content: center; + gap: 2rem; + flex-wrap: wrap; + margin: 1rem 0; +} + +.stat-item { + text-align: center; +} + +.stat-number { + font-size: 1.5em; + font-weight: bold; + color: #007bff; +} + +.stat-label { + font-size: 0.9em; + color: #666; +} + +/* Серии книг */ +.series-books article { + transition: transform 0.2s ease, box-shadow 0.2s ease; + border: 1px solid #e0e0e0; +} + +.series-books article:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0,0,0,0.1); +} + +.series-info { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 1rem; + border-radius: 8px; + margin-bottom: 2rem; +} + +.series-badge { + display: inline-block; + background: #007bff; + color: white; + padding: 0.2rem 0.5rem; + border-radius: 12px; + font-size: 0.8rem; + margin-left: 0.5rem; +} + +/* Dashboard */ +.dashboard-buttons { + display: flex; + gap: 10px; + margin-top: 1rem; + flex-wrap: nowrap; +} + +.dashboard-button { + flex: 1; + text-align: center; + padding: 0.75rem 0.5rem; + text-decoration: none; + border: 1px solid var(--secondary); + border-radius: 4px; + background: var(--secondary); + color: var(--secondary-inverse); + font-size: 0.9rem; + transition: all 0.3s ease; + min-height: 44px; + display: flex; + align-items: center; + justify-content: center; + white-space: nowrap; +} + +.dashboard-button:hover { + opacity: 0.9; + transform: translateY(-1px); +} + +.dashboard-button.new { + background: var(--primary); + border-color: var(--primary); + color: var(--primary-inverse); + flex: 0.7; +} + +.stats-list { + margin-top: 1rem; +} + +.stats-list p { + margin: 0.5rem 0; + padding: 0.3rem 0; + border-bottom: 1px solid #f0f0f0; +} + +.stats-list p:last-child { + border-bottom: none; +} + +.series-stats { + margin-top: 1rem; + padding: 1rem; + background: #f8f9fa; + border-radius: 5px; + border-left: 4px solid #6f42c1; +} + +.series-stats p { + margin: 0.5rem 0; + font-size: 0.9rem; +} + +.dashboard-section { + margin-top: 2rem; + padding-top: 1rem; + border-top: 1px solid #eee; +} + +.dashboard-item { + transition: transform 0.2s ease, box-shadow 0.2s ease; + border: 1px solid #e0e0e0; + padding: 1rem; +} + +.dashboard-item:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0,0,0,0.1); +} + +.welcome-message { + text-align: center; + padding: 3rem; + background: #f9f9f9; + border-radius: 8px; + margin-top: 2rem; +} + +.welcome-buttons { + display: flex; + gap: 1rem; + justify-content: center; + flex-wrap: wrap; + margin-top: 1.5rem; +} + +.action-buttons { + display: flex; + gap: 5px; + flex-wrap: wrap; + margin-top: 0.5rem; +} + +.action-buttons .compact-button { + flex: 1; + min-width: 80px; + text-align: center; + font-size: 0.8rem; + padding: 0.3rem 0.5rem; +} + +/* Улучшения для grid в dashboard */ +.grid { + gap: 1rem; +} + +.grid article { + margin: 0; + padding: 1.5rem; +} + +/* Адаптивность */ +@media (max-width: 768px) { + .adaptive-button { + flex: 1 1 calc(50% - 10px); + min-width: calc(50% - 10px); + font-size: 0.8rem; + padding: 10px 8px !important; + } + + .action-button { + padding: 0.6rem 1rem; + font-size: 0.85rem; + min-width: 120px; + height: 42px; + } + + .book-content { + font-size: 16px; + line-height: 1.6; + } + + .book-content h1 { + font-size: 1.6em; + } + + .book-content h2 { + font-size: 1.4em; + } + + .book-content h3 { + font-size: 1.2em; + } + + .book-content pre { + font-size: 14px; + } + + .author-books article { + flex-direction: column; + } + + .author-books .book-cover { + align-self: center; + } + + .avatar, .avatar-placeholder { + width: 120px; + height: 120px; + font-size: 2.5rem; + } + + .author-stats { + gap: 1rem; + } + + .stat-number { + font-size: 1.3em; + } + + .dashboard-buttons { + flex-direction: column; + gap: 8px; + } + + .dashboard-button { + flex: none; + width: 100%; + } + + .dashboard-button.new { + flex: none; + width: 100%; + } + + .welcome-buttons { + flex-direction: column; + align-items: center; + } + + .welcome-buttons a { + width: 100%; + max-width: 250px; + } + + .action-buttons { + flex-direction: column; + } + + .action-buttons .compact-button { + width: 100%; + } + + #fullscreen-controls { + top: 10px !important; + right: 10px !important; + } + + #fullscreen-controls button { + width: 60px !important; + height: 60px !important; + font-size: 24px !important; + border: 2px solid #ddd !important; + } + + .editor-controls button { + width: 50px !important; + height: 50px !important; + font-size: 20px !important; + } +} + +@media (max-width: 480px) { + .adaptive-button { + flex: 1 1 100%; + min-width: 100%; + } + + .action-button { + padding: 0.5rem 0.8rem; + font-size: 0.8rem; + min-width: 110px; + height: 40px; + } + + .action-buttons-container { + flex-direction: column; + width: 100%; + } + + .action-buttons-container .action-button { + width: 100%; + min-width: auto; + } + + .avatar, .avatar-placeholder { + width: 100px; + height: 100px; + font-size: 2rem; + } + + .author-stats { + flex-direction: column; + gap: 0.5rem; + } + + .dashboard-item { + padding: 0.8rem; + } + + .dashboard-button { + font-size: 0.85rem; + padding: 0.6rem 0.4rem; + } + + .welcome-message { + padding: 2rem 1rem; + } + + #fullscreen-controls button { + width: 55px !important; + height: 55px !important; + font-size: 22px !important; + } +} + +@media (min-width: 769px) { + #fullscreen-controls { + top: 15px !important; + right: 15px !important; + } + + #fullscreen-controls button { + width: 50px !important; + height: 50px !important; + font-size: 20px !important; + } +} + +/* Полноэкранные режимы редактора */ +#content.mobile-fullscreen { + position: fixed !important; + top: 50px !important; + left: 0 !important; + width: 100vw !important; + height: calc(100vh - 100px) !important; + z-index: 9998 !important; + background-color: white !important; + border: 2px solid #007bff !important; + border-radius: 0 !important; + font-size: 18px !important; + padding: 15px !important; + margin: 0 !important; + box-sizing: border-box !important; + resize: none !important; + box-shadow: none !important; + overflow-y: auto !important; + -webkit-overflow-scrolling: touch !important; +} + +#content.desktop-fullscreen { + position: fixed !important; + top: 5vh !important; + left: 5vw !important; + width: 90vw !important; + height: 90vh !important; + z-index: 9998 !important; + background-color: white !important; + border: 2px solid #007bff !important; + border-radius: 8px !important; + font-size: 16px !important; + padding: 20px !important; + margin: 0 !important; + box-sizing: border-box !important; + resize: none !important; + box-shadow: 0 0 20px rgba(0,0,0,0.3) !important; +} + +/* Стили для TinyMCE редактора */ +.tox-tinymce { + border-radius: 4px !important; + border: 1px solid #ddd !important; +} + +.tox-tinymce:focus { + border-color: #007bff !important; + box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25) !important; +} + +/* Адаптивность для TinyMCE */ +@media (max-width: 768px) { + .tox-tinymce { + border-radius: 0 !important; + } + + .tox-toolbar { + flex-wrap: wrap !important; + } +} + +.alert-info { + background: #d1ecf1; + color: #0c5460; + border: 1px solid #bee5eb; +} + +.alert-warning { + background: #fff3cd; + color: #856404; + border: 1px solid #ffeaa7; } \ No newline at end of file diff --git a/author.php b/author.php index c544d58..79dc9eb 100755 --- a/author.php +++ b/author.php @@ -1,192 +1,192 @@ -Неверный запрос"; - include 'views/footer.php'; - exit; -} - -$stmt = $pdo->prepare("SELECT id, username, display_name, avatar, bio FROM users WHERE id = ?"); -$stmt->execute([$author_id]); -$author = $stmt->fetch(PDO::FETCH_ASSOC); - -if (!$author) { - http_response_code(404); - echo "
Следите за обновлениями, скоро здесь появятся новые произведения!
-Следите за обновлениями, скоро здесь появятся новые произведения!
+Отправьте эту ссылку читателям для просмотра опубликованных глав:
- -- Примечание: В публичном просмотре отображаются только главы со статусом "Опубликована" -
-Экспортируйте книгу в различные форматы:
- - - -- Примечание: Экспортируются все главы книги (включая черновики) -
-| Название | -Статус | -Слов | -Действия | -
|---|---|---|---|
| = e($chapter['title']) ?> | -- - = $chapter['status'] == 'published' ? 'Опубликована' : 'Черновик' ?> - - | -= $chapter['word_count'] ?> | -- - Редактировать - - | -
В этой книге пока нет глав.
- - ✏️ Добавить первую главу - -+ Примечание: В публичном просмотре отображаются только главы со статусом "Опубликована" +
+Экспортируйте книгу в различные форматы:
+ + + ++ Примечание: Экспортируются все главы книги (включая черновики) +
+| Название | +Статус | +Слов | +Действия | +
|---|---|---|---|
| = e($chapter['title']) ?> | ++ + = $chapter['status'] == 'published' ? 'Опубликована' : 'Черновик' ?> + + | += $chapter['word_count'] ?> | ++ + Редактировать + + | +
В этой книге пока нет глав.
+ + ✏️ Добавить первую главу + +Создайте свою первую книгу и начните писать!
- 📖 Создать первую книгу -= e(mb_strimwidth($book['description'], 0, 150, '...')) ?>
- - - -Создайте свою первую книгу и начните писать!
+ 📖 Создать первую книгу += e(mb_strimwidth($book['description'], 0, 150, '...')) ?>
+ + + +Книга: = e($book['title']) ?>
- - -Книга: = e($book['title']) ?>
+ + +| № | -Название главы | -Статус | -Слов | -Обновлено | -Действия | -
|---|---|---|---|---|---|
| = $index + 1 ?> | -
- = e($chapter['title']) ?>
-
- = e(mb_strimwidth($chapter['description'], 0, 100, '...')) ?> - - |
- - - = $chapter['status'] == 'published' ? '✅ Опубликована' : '📝 Черновик' ?> - - | -= $chapter['word_count'] ?> | -- = date('d.m.Y H:i', strtotime($chapter['updated_at'])) ?> - | -
-
-
- ✏️
-
-
-
- |
-
| № | +Название главы | +Статус | +Слов | +Обновлено | +Действия | +
|---|---|---|---|---|---|
| = $index + 1 ?> | +
+ = e($chapter['title']) ?>
+
+ = e(mb_strimwidth($chapter['description'], 0, 100, '...')) ?> + + |
+ + + = $chapter['status'] == 'published' ? '✅ Опубликована' : '📝 Черновик' ?> + + | += $chapter['word_count'] ?> | ++ = date('d.m.Y H:i', strtotime($chapter['updated_at'])) ?> + | +
+
+
+ ✏️
+
+
+
+ |
+
Управляйте вашими книгами и главами
- -Книг: = count($books) ?>
-Глав: = $total_chapters ?>
-Всего слов: = $total_words ?>
- 0): ?> -Средняя глава: = round($total_words / max(1, $total_chapters)) ?> слов
- -Управляйте сериями книг
- - - 0): ?> -Книг в сериях: = $series_stats['total_books_in_series'] ?>
-Заполненных серий: = $series_stats['series_with_books'] ?>
-Глав: = $book['chapter_count'] ?> | Слов: = $book['total_words'] ?>
- -Книг: = count($books_in_series) ?> | Глав: = $series_chapters ?> | Слов: = $series_words ?>
- - -Управляйте вашими книгами и главами
+ +Книг: = count($books) ?>
+Глав: = $total_chapters ?>
+Всего слов: = $total_words ?>
+ 0): ?> +Средняя глава: = round($total_words / max(1, $total_chapters)) ?> слов
+ +Управляйте сериями книг
+ + + 0): ?> +Книг в сериях: = $series_stats['total_books_in_series'] ?>
+Заполненных серий: = $series_stats['series_with_books'] ?>
+Глав: = $book['chapter_count'] ?> | Слов: = $book['total_words'] ?>
+ +Книг: = count($books_in_series) ?> | Глав: = $series_chapters ?> | Слов: = $series_words ?>
+ + +Нет аккаунта? Зарегистрируйтесь здесь
-Нет аккаунта? Зарегистрируйтесь здесь
++ if (!preg_match('/<(p|div|h[1-6]|blockquote|pre|ul|ol|li)/i', $html)) { + // Разбиваем на строки и оборачиваем каждую непустую строку в
+ $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[] = "
{$line}
"; + } + } + } + + $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>/is', "$1\n\n", $markdown);
+
+ // Обрабатываем разрывы строк
+ $markdown = preg_replace('/
]*>\s*<\/br[^>]*>/i', "\n", $markdown);
+ $markdown = preg_replace('/
]*>/i', " \n", $markdown); // Два пробела для Markdown разрыва
+
+ // Заголовки
+ $markdown = preg_replace('/
]*>(.*?)<\/blockquote>/is', "> $1\n", $markdown);
+
+ // Код
+ $markdown = preg_replace('/]*>(.*?)<\/code>/is', '`$1`', $markdown);
+ $markdown = preg_replace('/]*>(.*?)<\/pre>/is', "```\n$1\n```", $markdown);
+
+ // Ссылки
+ $markdown = preg_replace('/]*href="([^"]*)"[^>]*>(.*?)<\/a>/is', '[$2]($1)', $markdown);
+
+ // Изображения
+ $markdown = preg_replace('/
]*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>/is', "$1
", $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 - оборачиваем текст без тегов в
+ if (!preg_match('/<[^>]+>/', $html) && trim($html) !== '') {
+ // Если нет HTML тегов, оборачиваем в
+ $lines = explode("\n", trim($html));
+ $wrapped = array_map(function($line) {
+ $line = trim($line);
+ return $line ? "
{$line}
" : '';
+ }, $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);
+ }
+}
?>
\ No newline at end of file
diff --git a/models/Series.php b/models/Series.php
old mode 100755
new mode 100644
index 6385abd..d53e2d8
--- a/models/Series.php
+++ b/models/Series.php
@@ -1,158 +1,158 @@
-pdo = $pdo;
- }
-
- public function findById($id) {
- $stmt = $this->pdo->prepare("
- SELECT s.*,
- COUNT(b.id) as book_count,
- COALESCE((
- SELECT SUM(c.word_count)
- FROM chapters c
- JOIN books b2 ON c.book_id = b2.id
- WHERE b2.series_id = s.id AND b2.published = 1
- ), 0) as total_words
- FROM series s
- LEFT JOIN books b ON s.id = b.series_id AND b.published = 1
- WHERE s.id = ?
- GROUP BY s.id
- ");
- $stmt->execute([$id]);
- return $stmt->fetch(PDO::FETCH_ASSOC);
- }
-
- public function findByUser($user_id, $include_stats = true) {
- if ($include_stats) {
- $sql = "
- SELECT s.*,
- COUNT(b.id) as book_count,
- COALESCE((
- SELECT SUM(c.word_count)
- FROM chapters c
- JOIN books b2 ON c.book_id = b2.id
- WHERE b2.series_id = s.id AND b2.user_id = ?
- ), 0) as total_words
- FROM series s
- LEFT JOIN books b ON s.id = b.series_id
- WHERE s.user_id = ?
- GROUP BY s.id
- ORDER BY s.created_at DESC
- ";
- $stmt = $this->pdo->prepare($sql);
- $stmt->execute([$user_id, $user_id]);
- } else {
- $sql = "SELECT * FROM series WHERE user_id = ? ORDER BY created_at DESC";
- $stmt = $this->pdo->prepare($sql);
- $stmt->execute([$user_id]);
- }
-
- return $stmt->fetchAll(PDO::FETCH_ASSOC);
- }
-
- public function create($data) {
- $stmt = $this->pdo->prepare("
- INSERT INTO series (title, description, user_id)
- VALUES (?, ?, ?)
- ");
- return $stmt->execute([
- $data['title'],
- $data['description'] ?? null,
- $data['user_id']
- ]);
- }
-
- public function update($id, $data) {
- $stmt = $this->pdo->prepare("
- UPDATE series
- SET title = ?, description = ?, updated_at = CURRENT_TIMESTAMP
- WHERE id = ? AND user_id = ?
- ");
- return $stmt->execute([
- $data['title'],
- $data['description'] ?? null,
- $id,
- $data['user_id']
- ]);
- }
-
- public function delete($id, $user_id) {
- try {
- $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->execute([$id, $user_id]);
-
- $stmt = $this->pdo->prepare("DELETE FROM series WHERE id = ? AND user_id = ?");
- $result = $stmt->execute([$id, $user_id]);
-
- $this->pdo->commit();
- return $result;
- } catch (Exception $e) {
- $this->pdo->rollBack();
- return false;
- }
- }
-
- public function userOwnsSeries($series_id, $user_id) {
- $stmt = $this->pdo->prepare("SELECT id FROM series WHERE id = ? AND user_id = ?");
- $stmt->execute([$series_id, $user_id]);
- return $stmt->fetch() !== false;
- }
-
- public function getBooksInSeries($series_id, $only_published = false) {
- $sql = "SELECT * FROM books WHERE series_id = ?";
- if ($only_published) {
- $sql .= " AND published = 1";
- }
- $sql .= " ORDER BY sort_order_in_series, created_at";
-
- $stmt = $this->pdo->prepare($sql);
- $stmt->execute([$series_id]);
- return $stmt->fetchAll(PDO::FETCH_ASSOC);
- }
-
- public function getNextSortOrder($series_id) {
- $stmt = $this->pdo->prepare("SELECT MAX(sort_order_in_series) as max_order FROM books WHERE series_id = ?");
- $stmt->execute([$series_id]);
- $result = $stmt->fetch();
- return ($result['max_order'] ?? 0) + 1;
- }
-
- public function getSeriesStats($series_id, $user_id = null) {
- $sql = "
- SELECT
- COUNT(b.id) as book_count,
- COALESCE(SUM(stats.chapter_count), 0) as chapter_count,
- COALESCE(SUM(stats.total_words), 0) as total_words
- FROM series s
- LEFT JOIN books b ON s.id = b.series_id
- LEFT JOIN (
- SELECT
- book_id,
- COUNT(id) as chapter_count,
- SUM(word_count) as total_words
- FROM chapters
- GROUP BY book_id
- ) stats ON b.id = stats.book_id
- WHERE s.id = ?
- ";
-
- $params = [$series_id];
-
- if ($user_id) {
- $sql .= " AND s.user_id = ?";
- $params[] = $user_id;
- }
-
- $stmt = $this->pdo->prepare($sql);
- $stmt->execute($params);
- return $stmt->fetch(PDO::FETCH_ASSOC);
- }
-}
+pdo = $pdo;
+ }
+
+ public function findById($id) {
+ $stmt = $this->pdo->prepare("
+ SELECT s.*,
+ COUNT(b.id) as book_count,
+ COALESCE((
+ SELECT SUM(c.word_count)
+ FROM chapters c
+ JOIN books b2 ON c.book_id = b2.id
+ WHERE b2.series_id = s.id AND b2.published = 1
+ ), 0) as total_words
+ FROM series s
+ LEFT JOIN books b ON s.id = b.series_id AND b.published = 1
+ WHERE s.id = ?
+ GROUP BY s.id
+ ");
+ $stmt->execute([$id]);
+ return $stmt->fetch(PDO::FETCH_ASSOC);
+ }
+
+ public function findByUser($user_id, $include_stats = true) {
+ if ($include_stats) {
+ $sql = "
+ SELECT s.*,
+ COUNT(b.id) as book_count,
+ COALESCE((
+ SELECT SUM(c.word_count)
+ FROM chapters c
+ JOIN books b2 ON c.book_id = b2.id
+ WHERE b2.series_id = s.id AND b2.user_id = ?
+ ), 0) as total_words
+ FROM series s
+ LEFT JOIN books b ON s.id = b.series_id
+ WHERE s.user_id = ?
+ GROUP BY s.id
+ ORDER BY s.created_at DESC
+ ";
+ $stmt = $this->pdo->prepare($sql);
+ $stmt->execute([$user_id, $user_id]);
+ } else {
+ $sql = "SELECT * FROM series WHERE user_id = ? ORDER BY created_at DESC";
+ $stmt = $this->pdo->prepare($sql);
+ $stmt->execute([$user_id]);
+ }
+
+ return $stmt->fetchAll(PDO::FETCH_ASSOC);
+ }
+
+ public function create($data) {
+ $stmt = $this->pdo->prepare("
+ INSERT INTO series (title, description, user_id)
+ VALUES (?, ?, ?)
+ ");
+ return $stmt->execute([
+ $data['title'],
+ $data['description'] ?? null,
+ $data['user_id']
+ ]);
+ }
+
+ public function update($id, $data) {
+ $stmt = $this->pdo->prepare("
+ UPDATE series
+ SET title = ?, description = ?, updated_at = CURRENT_TIMESTAMP
+ WHERE id = ? AND user_id = ?
+ ");
+ return $stmt->execute([
+ $data['title'],
+ $data['description'] ?? null,
+ $id,
+ $data['user_id']
+ ]);
+ }
+
+ public function delete($id, $user_id) {
+ try {
+ $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->execute([$id, $user_id]);
+
+ $stmt = $this->pdo->prepare("DELETE FROM series WHERE id = ? AND user_id = ?");
+ $result = $stmt->execute([$id, $user_id]);
+
+ $this->pdo->commit();
+ return $result;
+ } catch (Exception $e) {
+ $this->pdo->rollBack();
+ return false;
+ }
+ }
+
+ public function userOwnsSeries($series_id, $user_id) {
+ $stmt = $this->pdo->prepare("SELECT id FROM series WHERE id = ? AND user_id = ?");
+ $stmt->execute([$series_id, $user_id]);
+ return $stmt->fetch() !== false;
+ }
+
+ public function getBooksInSeries($series_id, $only_published = false) {
+ $sql = "SELECT * FROM books WHERE series_id = ?";
+ if ($only_published) {
+ $sql .= " AND published = 1";
+ }
+ $sql .= " ORDER BY sort_order_in_series, created_at";
+
+ $stmt = $this->pdo->prepare($sql);
+ $stmt->execute([$series_id]);
+ return $stmt->fetchAll(PDO::FETCH_ASSOC);
+ }
+
+ public function getNextSortOrder($series_id) {
+ $stmt = $this->pdo->prepare("SELECT MAX(sort_order_in_series) as max_order FROM books WHERE series_id = ?");
+ $stmt->execute([$series_id]);
+ $result = $stmt->fetch();
+ return ($result['max_order'] ?? 0) + 1;
+ }
+
+ public function getSeriesStats($series_id, $user_id = null) {
+ $sql = "
+ SELECT
+ COUNT(b.id) as book_count,
+ COALESCE(SUM(stats.chapter_count), 0) as chapter_count,
+ COALESCE(SUM(stats.total_words), 0) as total_words
+ FROM series s
+ LEFT JOIN books b ON s.id = b.series_id
+ LEFT JOIN (
+ SELECT
+ book_id,
+ COUNT(id) as chapter_count,
+ SUM(word_count) as total_words
+ FROM chapters
+ GROUP BY book_id
+ ) stats ON b.id = stats.book_id
+ WHERE s.id = ?
+ ";
+
+ $params = [$series_id];
+
+ if ($user_id) {
+ $sql .= " AND s.user_id = ?";
+ $params[] = $user_id;
+ }
+
+ $stmt = $this->pdo->prepare($sql);
+ $stmt->execute($params);
+ return $stmt->fetch(PDO::FETCH_ASSOC);
+ }
+}
?>
\ No newline at end of file
diff --git a/models/User.php b/models/User.php
index 046aeca..fea8b84 100755
--- a/models/User.php
+++ b/models/User.php
@@ -1,119 +1,119 @@
-pdo = $pdo;
- }
-
- public function findById($id) {
- $stmt = $this->pdo->prepare("SELECT * FROM users WHERE id = ?");
- $stmt->execute([$id]);
- return $stmt->fetch(PDO::FETCH_ASSOC);
- }
-
- public function findByUsername($username) {
- $stmt = $this->pdo->prepare("SELECT * FROM users WHERE username = ?");
- $stmt->execute([$username]);
- return $stmt->fetch(PDO::FETCH_ASSOC);
- }
-
- public function findByEmail($email) {
- $stmt = $this->pdo->prepare("SELECT * FROM users WHERE email = ?");
- $stmt->execute([$email]);
- return $stmt->fetch(PDO::FETCH_ASSOC);
- }
-
- 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->execute();
- return $stmt->fetchAll(PDO::FETCH_ASSOC);
- }
-
- public function create($data) {
- $password_hash = password_hash($data['password'], PASSWORD_DEFAULT);
-
- $is_active = $data['is_active'] ?? 0;
-
- $stmt = $this->pdo->prepare("
- INSERT INTO users (username, display_name, email, password_hash, is_active)
- VALUES (?, ?, ?, ?, ?)
- ");
-
- return $stmt->execute([
- $data['username'],
- $data['display_name'] ?? $data['username'],
- $data['email'] ?? null,
- $password_hash,
- $is_active
- ]);
- }
-
- public function update($id, $data) {
- $sql = "UPDATE users SET display_name = ?, email = ?";
- $params = [$data['display_name'], $data['email']];
-
- if (!empty($data['password'])) {
- $sql .= ", password_hash = ?";
- $params[] = password_hash($data['password'], PASSWORD_DEFAULT);
- }
-
- $sql .= " WHERE id = ?";
- $params[] = $id;
-
- $stmt = $this->pdo->prepare($sql);
- return $stmt->execute($params);
- }
-
- public function updateStatus($id, $is_active) {
- $stmt = $this->pdo->prepare("UPDATE users SET is_active = ? WHERE id = ?");
- return $stmt->execute([$is_active, $id]);
- }
-
- public function delete($id) {
- $stmt = $this->pdo->prepare("DELETE FROM users WHERE id = ?");
- return $stmt->execute([$id]);
- }
-
- public function updateLastLogin($id) {
- $stmt = $this->pdo->prepare("UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?");
- return $stmt->execute([$id]);
- }
-
- public function verifyPassword($password, $hash) {
- return password_verify($password, $hash);
- }
-
- public function updateAvatar($id, $filename) {
- $stmt = $this->pdo->prepare("UPDATE users SET avatar = ? WHERE id = ?");
- return $stmt->execute([$filename, $id]);
- }
-
- public function updateBio($id, $bio) {
- $stmt = $this->pdo->prepare("UPDATE users SET bio = ? WHERE id = ?");
- return $stmt->execute([$bio, $id]);
- }
-
- public function updateProfile($id, $data) {
- $sql = "UPDATE users SET display_name = ?, email = ?, bio = ?";
- $params = [
- $data['display_name'] ?? '',
- $data['email'] ?? null,
- $data['bio'] ?? null
- ];
-
- if (!empty($data['avatar'])) {
- $sql .= ", avatar = ?";
- $params[] = $data['avatar'];
- }
-
- $sql .= " WHERE id = ?";
- $params[] = $id;
-
- $stmt = $this->pdo->prepare($sql);
- return $stmt->execute($params);
- }
-}
+pdo = $pdo;
+ }
+
+ public function findById($id) {
+ $stmt = $this->pdo->prepare("SELECT * FROM users WHERE id = ?");
+ $stmt->execute([$id]);
+ return $stmt->fetch(PDO::FETCH_ASSOC);
+ }
+
+ public function findByUsername($username) {
+ $stmt = $this->pdo->prepare("SELECT * FROM users WHERE username = ?");
+ $stmt->execute([$username]);
+ return $stmt->fetch(PDO::FETCH_ASSOC);
+ }
+
+ public function findByEmail($email) {
+ $stmt = $this->pdo->prepare("SELECT * FROM users WHERE email = ?");
+ $stmt->execute([$email]);
+ return $stmt->fetch(PDO::FETCH_ASSOC);
+ }
+
+ 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->execute();
+ return $stmt->fetchAll(PDO::FETCH_ASSOC);
+ }
+
+ public function create($data) {
+ $password_hash = password_hash($data['password'], PASSWORD_DEFAULT);
+
+ $is_active = $data['is_active'] ?? 0;
+
+ $stmt = $this->pdo->prepare("
+ INSERT INTO users (username, display_name, email, password_hash, is_active)
+ VALUES (?, ?, ?, ?, ?)
+ ");
+
+ return $stmt->execute([
+ $data['username'],
+ $data['display_name'] ?? $data['username'],
+ $data['email'] ?? null,
+ $password_hash,
+ $is_active
+ ]);
+ }
+
+ public function update($id, $data) {
+ $sql = "UPDATE users SET display_name = ?, email = ?";
+ $params = [$data['display_name'], $data['email']];
+
+ if (!empty($data['password'])) {
+ $sql .= ", password_hash = ?";
+ $params[] = password_hash($data['password'], PASSWORD_DEFAULT);
+ }
+
+ $sql .= " WHERE id = ?";
+ $params[] = $id;
+
+ $stmt = $this->pdo->prepare($sql);
+ return $stmt->execute($params);
+ }
+
+ public function updateStatus($id, $is_active) {
+ $stmt = $this->pdo->prepare("UPDATE users SET is_active = ? WHERE id = ?");
+ return $stmt->execute([$is_active, $id]);
+ }
+
+ public function delete($id) {
+ $stmt = $this->pdo->prepare("DELETE FROM users WHERE id = ?");
+ return $stmt->execute([$id]);
+ }
+
+ public function updateLastLogin($id) {
+ $stmt = $this->pdo->prepare("UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?");
+ return $stmt->execute([$id]);
+ }
+
+ public function verifyPassword($password, $hash) {
+ return password_verify($password, $hash);
+ }
+
+ public function updateAvatar($id, $filename) {
+ $stmt = $this->pdo->prepare("UPDATE users SET avatar = ? WHERE id = ?");
+ return $stmt->execute([$filename, $id]);
+ }
+
+ public function updateBio($id, $bio) {
+ $stmt = $this->pdo->prepare("UPDATE users SET bio = ? WHERE id = ?");
+ return $stmt->execute([$bio, $id]);
+ }
+
+ public function updateProfile($id, $data) {
+ $sql = "UPDATE users SET display_name = ?, email = ?, bio = ?";
+ $params = [
+ $data['display_name'] ?? '',
+ $data['email'] ?? null,
+ $data['bio'] ?? null
+ ];
+
+ if (!empty($data['avatar'])) {
+ $sql .= ", avatar = ?";
+ $params[] = $data['avatar'];
+ }
+
+ $sql .= " WHERE id = ?";
+ $params[] = $id;
+
+ $stmt = $this->pdo->prepare($sql);
+ return $stmt->execute($params);
+ }
+}
?>
\ No newline at end of file
diff --git a/preview.php b/preview.php
index 47ddb44..0f0446f 100755
--- a/preview.php
+++ b/preview.php
@@ -7,9 +7,15 @@ $Parsedown = new ParsedownExtra();;
$content = $_POST['content'] ?? '';
$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);
?>
diff --git a/profile.php b/profile.php
index 3b14bb6..838790c 100755
--- a/profile.php
+++ b/profile.php
@@ -1,195 +1,195 @@
-findById($user_id);
-
-$message = '';
-$avatar_error = '';
-
-if ($_SERVER['REQUEST_METHOD'] === 'POST') {
- if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
- $message = "Ошибка безопасности";
- } else {
- $display_name = trim($_POST['display_name'] ?? '');
- $email = trim($_POST['email'] ?? '');
- $bio = trim($_POST['bio'] ?? '');
-
- // Обработка загрузки аватарки
- if (isset($_FILES['avatar']) && $_FILES['avatar']['error'] === UPLOAD_ERR_OK) {
- $avatar_result = handleAvatarUpload($_FILES['avatar'], $user_id);
- if ($avatar_result['success']) {
- $userModel->updateAvatar($user_id, $avatar_result['filename']);
- // Обновляем данные пользователя
- $user = $userModel->findById($user_id);
- } else {
- $avatar_error = $avatar_result['error'];
- }
- }
-
- // Обработка удаления аватарки
- if (isset($_POST['delete_avatar']) && $_POST['delete_avatar'] == '1') {
- deleteUserAvatar($user_id);
- $user = $userModel->findById($user_id);
- }
-
- // Обновляем основные данные
- $data = [
- 'display_name' => $display_name,
- 'email' => $email,
- 'bio' => $bio
- ];
-
- if ($userModel->updateProfile($user_id, $data)) {
- $_SESSION['display_name'] = $display_name ?: $user['username'];
- $message = "Профиль обновлен";
- // Обновляем данные пользователя
- $user = $userModel->findById($user_id);
- } else {
- $message = "Ошибка при обновлении профиля";
- }
- }
-}
-
-$page_title = "Мой профиль";
-include 'views/header.php';
-?>
-
-Мой профиль
-
-
-
-
-
-
-
- Основная информация
-
-
-
-
- Аватарка
-
-
-
-
-
-
- = mb_substr(e($user['display_name'] ?? $user['username']), 0, 1) ?>
-
-
-
-
-
-
-
-
-
- Примечание: Аватарка отображается на вашей публичной странице автора
-
-
-
-
-
-
-
- Информация об аккаунте
-
- 👁️ Посмотреть мою публичную страницу
-
- Дата регистрации: = date('d.m.Y H:i', strtotime($user['created_at'])) ?>
-
- Последний вход: = date('d.m.Y H:i', strtotime($user['last_login'])) ?>
-
-
-
+findById($user_id);
+
+$message = '';
+$avatar_error = '';
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
+ $message = "Ошибка безопасности";
+ } else {
+ $display_name = trim($_POST['display_name'] ?? '');
+ $email = trim($_POST['email'] ?? '');
+ $bio = trim($_POST['bio'] ?? '');
+
+ // Обработка загрузки аватарки
+ if (isset($_FILES['avatar']) && $_FILES['avatar']['error'] === UPLOAD_ERR_OK) {
+ $avatar_result = handleAvatarUpload($_FILES['avatar'], $user_id);
+ if ($avatar_result['success']) {
+ $userModel->updateAvatar($user_id, $avatar_result['filename']);
+ // Обновляем данные пользователя
+ $user = $userModel->findById($user_id);
+ } else {
+ $avatar_error = $avatar_result['error'];
+ }
+ }
+
+ // Обработка удаления аватарки
+ if (isset($_POST['delete_avatar']) && $_POST['delete_avatar'] == '1') {
+ deleteUserAvatar($user_id);
+ $user = $userModel->findById($user_id);
+ }
+
+ // Обновляем основные данные
+ $data = [
+ 'display_name' => $display_name,
+ 'email' => $email,
+ 'bio' => $bio
+ ];
+
+ if ($userModel->updateProfile($user_id, $data)) {
+ $_SESSION['display_name'] = $display_name ?: $user['username'];
+ $message = "Профиль обновлен";
+ // Обновляем данные пользователя
+ $user = $userModel->findById($user_id);
+ } else {
+ $message = "Ошибка при обновлении профиля";
+ }
+ }
+}
+
+$page_title = "Мой профиль";
+include 'views/header.php';
+?>
+
+Мой профиль
+
+
+
+
+
+
+
+ Основная информация
+
+
+
+
+ Аватарка
+
+
+
+
+
+
+ = mb_substr(e($user['display_name'] ?? $user['username']), 0, 1) ?>
+
+
+
+
+
+
+
+
+
+ Примечание: Аватарка отображается на вашей публичной странице автора
+
+
+
+
+
+
+
+ Информация об аккаунте
+
+ 👁️ Посмотреть мою публичную страницу
+
+ Дата регистрации: = date('d.m.Y H:i', strtotime($user['created_at'])) ?>
+
+ Последний вход: = date('d.m.Y H:i', strtotime($user['last_login'])) ?>
+
+
+
\ No newline at end of file
diff --git a/series.php b/series.php
old mode 100755
new mode 100644
index bb02401..9f90f28
--- a/series.php
+++ b/series.php
@@ -1,95 +1,95 @@
-findByUser($user_id);
-
-// Получаем статистику для каждой серии отдельно
-foreach ($series as &$ser) {
- $stats = $seriesModel->getSeriesStats($ser['id'], $user_id);
- $ser['book_count'] = $stats['book_count'] ?? 0;
- $ser['total_words'] = $stats['total_words'] ?? 0;
-}
-unset($ser);
-
-$page_title = "Мои серии книг";
-include 'views/header.php';
-?>
-
-Мои серии книг
-
-
-
- = e($_SESSION['success']) ?>
-
-
-
-
-
-
- = e($_SESSION['error']) ?>
-
-
-
-
-
- Всего серий: = count($series) ?>
- ➕ Новая серия
-
-
-
-
- У вас пока нет серий книг
- Создайте свою первую серию для организации книг!
- 📚 Создать первую серию
-
-
-
-
-
-
-
- = e($ser['title']) ?>
-
-
-
-
-
- = e(mb_strimwidth($ser['description'], 0, 200, '...')) ?>
-
-
-
-
-
-
-
-
+findByUser($user_id);
+
+// Получаем статистику для каждой серии отдельно
+foreach ($series as &$ser) {
+ $stats = $seriesModel->getSeriesStats($ser['id'], $user_id);
+ $ser['book_count'] = $stats['book_count'] ?? 0;
+ $ser['total_words'] = $stats['total_words'] ?? 0;
+}
+unset($ser);
+
+$page_title = "Мои серии книг";
+include 'views/header.php';
+?>
+
+Мои серии книг
+
+
+
+ = e($_SESSION['success']) ?>
+
+
+
+
+
+
+ = e($_SESSION['error']) ?>
+
+
+
+
+
+ Всего серий: = count($series) ?>
+ ➕ Новая серия
+
+
+
+
+ У вас пока нет серий книг
+ Создайте свою первую серию для организации книг!
+ 📚 Создать первую серию
+
+
+
+
+
+
+
+ = e($ser['title']) ?>
+
+
+
+
+
+ = e(mb_strimwidth($ser['description'], 0, 200, '...')) ?>
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/series_delete.php b/series_delete.php
old mode 100755
new mode 100644
index 1bdd740..372093f
--- a/series_delete.php
+++ b/series_delete.php
@@ -1,39 +1,39 @@
-userOwnsSeries($series_id, $user_id)) {
- $_SESSION['error'] = "У вас нет доступа к этой серии";
- redirect('series.php');
-}
-
-$series = $seriesModel->findById($series_id);
-
-if ($seriesModel->delete($series_id, $user_id)) {
- $_SESSION['success'] = "Серия «" . e($series['title']) . "» успешно удалена";
-} else {
- $_SESSION['error'] = "Ошибка при удалении серии";
-}
-
-redirect('series.php');
+userOwnsSeries($series_id, $user_id)) {
+ $_SESSION['error'] = "У вас нет доступа к этой серии";
+ redirect('series.php');
+}
+
+$series = $seriesModel->findById($series_id);
+
+if ($seriesModel->delete($series_id, $user_id)) {
+ $_SESSION['success'] = "Серия «" . e($series['title']) . "» успешно удалена";
+} else {
+ $_SESSION['error'] = "Ошибка при удалении серии";
+}
+
+redirect('series.php');
?>
\ No newline at end of file
diff --git a/series_edit.php b/series_edit.php
old mode 100755
new mode 100644
index a9b1ac2..e84a039
--- a/series_edit.php
+++ b/series_edit.php
@@ -1,179 +1,179 @@
-findById($series_id);
- if (!$series || !$seriesModel->userOwnsSeries($series_id, $user_id)) {
- $_SESSION['error'] = "Серия не найдена или у вас нет доступа";
- redirect('series.php');
- }
- $is_edit = true;
-}
-
-if ($_SERVER['REQUEST_METHOD'] === 'POST') {
- if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
- $_SESSION['error'] = "Ошибка безопасности";
- redirect($is_edit ? "series_edit.php?id=$series_id" : 'series_edit.php');
- }
-
- $title = trim($_POST['title'] ?? '');
- $description = trim($_POST['description'] ?? '');
-
- if (empty($title)) {
- $_SESSION['error'] = "Название серии обязательно";
- } else {
- $data = [
- 'title' => $title,
- 'description' => $description,
- 'user_id' => $user_id
- ];
-
- if ($is_edit) {
- $success = $seriesModel->update($series_id, $data);
- $message = $success ? "Серия успешно обновлена" : "Ошибка при обновлении серии";
- } else {
- $success = $seriesModel->create($data);
- $message = $success ? "Серия успешно создана" : "Ошибка при создании серии";
-
- if ($success) {
- $new_series_id = $pdo->lastInsertId();
- redirect("series_edit.php?id=$new_series_id");
- }
- }
-
- if ($success) {
- $_SESSION['success'] = $message;
- redirect('series.php');
- } else {
- $_SESSION['error'] = $message;
- }
- }
-}
-
-$page_title = $is_edit ? "Редактирование серии" : "Создание новой серии";
-include 'views/header.php';
-?>
-
-= $is_edit ? "Редактирование серии" : "Создание новой серии" ?>
-
-
-
- = e($_SESSION['error']) ?>
-
-
-
-
-
-
-
-
- Книги в этой серии
-
- findBySeries($series_id);
-
- // Вычисляем общую статистику
- $total_chapters = 0;
- $total_words = 0;
- foreach ($books_in_series as $book) {
- $stats = $bookModel->getBookStats($book['id']);
- $total_chapters += $stats['chapter_count'] ?? 0;
- $total_words += $stats['total_words'] ?? 0;
- }
- ?>
-
-
-
- В этой серии пока нет книг.
- 📚 Добавить книги
-
-
-
-
-
-
- Порядок
- Название книги
- Жанр
- Статус
- Действия
-
-
-
-
-
- = $book['sort_order_in_series'] ?>
-
- = e($book['title']) ?>
-
-
= e(mb_strimwidth($book['description'], 0, 100, '...')) ?>
-
-
- = e($book['genre']) ?>
-
-
- = $book['published'] ? '✅ Опубликована' : '📝 Черновик' ?>
-
-
-
-
- Редактировать
-
-
-
-
-
-
-
-
-
- Статистика серии:
- Книг: = count($books_in_series) ?> |
- Глав: = $total_chapters ?> |
- Слов: = $total_words ?>
-
-
-
-
-
+findById($series_id);
+ if (!$series || !$seriesModel->userOwnsSeries($series_id, $user_id)) {
+ $_SESSION['error'] = "Серия не найдена или у вас нет доступа";
+ redirect('series.php');
+ }
+ $is_edit = true;
+}
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
+ $_SESSION['error'] = "Ошибка безопасности";
+ redirect($is_edit ? "series_edit.php?id=$series_id" : 'series_edit.php');
+ }
+
+ $title = trim($_POST['title'] ?? '');
+ $description = trim($_POST['description'] ?? '');
+
+ if (empty($title)) {
+ $_SESSION['error'] = "Название серии обязательно";
+ } else {
+ $data = [
+ 'title' => $title,
+ 'description' => $description,
+ 'user_id' => $user_id
+ ];
+
+ if ($is_edit) {
+ $success = $seriesModel->update($series_id, $data);
+ $message = $success ? "Серия успешно обновлена" : "Ошибка при обновлении серии";
+ } else {
+ $success = $seriesModel->create($data);
+ $message = $success ? "Серия успешно создана" : "Ошибка при создании серии";
+
+ if ($success) {
+ $new_series_id = $pdo->lastInsertId();
+ redirect("series_edit.php?id=$new_series_id");
+ }
+ }
+
+ if ($success) {
+ $_SESSION['success'] = $message;
+ redirect('series.php');
+ } else {
+ $_SESSION['error'] = $message;
+ }
+ }
+}
+
+$page_title = $is_edit ? "Редактирование серии" : "Создание новой серии";
+include 'views/header.php';
+?>
+
+= $is_edit ? "Редактирование серии" : "Создание новой серии" ?>
+
+
+
+ = e($_SESSION['error']) ?>
+
+
+
+
+
+
+
+
+ Книги в этой серии
+
+ findBySeries($series_id);
+
+ // Вычисляем общую статистику
+ $total_chapters = 0;
+ $total_words = 0;
+ foreach ($books_in_series as $book) {
+ $stats = $bookModel->getBookStats($book['id']);
+ $total_chapters += $stats['chapter_count'] ?? 0;
+ $total_words += $stats['total_words'] ?? 0;
+ }
+ ?>
+
+
+
+ В этой серии пока нет книг.
+ 📚 Добавить книги
+
+
+
+
+
+
+ Порядок
+ Название книги
+ Жанр
+ Статус
+ Действия
+
+
+
+
+
+ = $book['sort_order_in_series'] ?>
+
+ = e($book['title']) ?>
+
+
= e(mb_strimwidth($book['description'], 0, 100, '...')) ?>
+
+
+ = e($book['genre']) ?>
+
+
+ = $book['published'] ? '✅ Опубликована' : '📝 Черновик' ?>
+
+
+
+
+ Редактировать
+
+
+
+
+
+
+
+
+
+ Статистика серии:
+ Книг: = count($books_in_series) ?> |
+ Глав: = $total_chapters ?> |
+ Слов: = $total_words ?>
+
+
+
+
+
\ No newline at end of file
diff --git a/view_book.php b/view_book.php
index a96959f..2fb01e0 100755
--- a/view_book.php
+++ b/view_book.php
@@ -1,270 +1,276 @@
-findByShareToken($share_token);
-} elseif ($book_id) {
- $book = $bookModel->findById($book_id);
-}
-
-if (!$book) {
- http_response_code(404);
- $page_title = "Книга не найдена";
- include 'views/header.php';
- ?>
-
-
- Книга не найдена
- Запрошенная книга не существует или была удалена.
- На главную
-
-
- getPublishedChapters($book['id']);
-$total_words = array_sum(array_column($chapters, 'word_count'));
-
-// Получаем информацию об авторе
-$stmt = $pdo->prepare("SELECT display_name, username FROM users WHERE id = ?");
-$stmt->execute([$book['user_id']]);
-$author_info = $stmt->fetch(PDO::FETCH_ASSOC);
-$author_name = $author_info['display_name'] ?? $author_info['username'] ?? 'Неизвестный автор';
-
-$page_title = $book['title'];
-include 'views/header.php';
-?>
-
-
-
-
-
-
-
-
-
-
- = e($book['title']) ?>
-
- prepare("SELECT id, title FROM series WHERE id = ?");
- $series_stmt->execute([$book['series_id']]);
- $series = $series_stmt->fetch();
- ?>
-
-
- 📚 Часть серии:
-
- = e($series['title']) ?>
-
- (Книга = $book['sort_order_in_series'] ?>)
-
-
-
-
-
- = e($author_name) ?>
-
-
-
- Жанр: = e($book['genre']) ?>
-
-
-
-
-
- = nl2br(e($book['description'])) ?>
-
-
-
-
-
-
-
-
-
-
-
-
- Экспорт книги
-
-
-
-
- Примечание: Экспортируются только опубликованные главы
-
-
-
-
-
- В этой книге пока нет опубликованных глав
- Автор еще не опубликовал ни одной главы
-
-
-
-
-
-
-
-
-
-
-
+findByShareToken($share_token);
+} elseif ($book_id) {
+ $book = $bookModel->findById($book_id);
+}
+
+if (!$book) {
+ http_response_code(404);
+ $page_title = "Книга не найдена";
+ include 'views/header.php';
+ ?>
+
+
+ Книга не найдена
+ Запрошенная книга не существует или была удалена.
+ На главную
+
+
+ getPublishedChapters($book['id']);
+$total_words = array_sum(array_column($chapters, 'word_count'));
+
+// Получаем информацию об авторе
+$stmt = $pdo->prepare("SELECT display_name, username FROM users WHERE id = ?");
+$stmt->execute([$book['user_id']]);
+$author_info = $stmt->fetch(PDO::FETCH_ASSOC);
+$author_name = $author_info['display_name'] ?? $author_info['username'] ?? 'Неизвестный автор';
+
+$page_title = $book['title'];
+include 'views/header.php';
+?>
+
+
+
+
+
+
+
+
+
+
+ = e($book['title']) ?>
+
+ prepare("SELECT id, title FROM series WHERE id = ?");
+ $series_stmt->execute([$book['series_id']]);
+ $series = $series_stmt->fetch();
+ ?>
+
+
+ 📚 Часть серии:
+
+ = e($series['title']) ?>
+
+ (Книга = $book['sort_order_in_series'] ?>)
+
+
+
+
+
+ = e($author_name) ?>
+
+
+
+ Жанр: = e($book['genre']) ?>
+
+
+
+
+
+ = nl2br(e($book['description'])) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+ Экспорт книги
+
+
+
+
+ Примечание: Экспортируются только опубликованные главы
+
+
+
+
+
+ В этой книге пока нет опубликованных глав
+ Автор еще не опубликовал ни одной главы
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/view_series.php b/view_series.php
old mode 100755
new mode 100644
index 878ce61..63fe8ca
--- a/view_series.php
+++ b/view_series.php
@@ -1,161 +1,161 @@
-Неверный запрос";
- include 'views/footer.php';
- exit;
-}
-
-$seriesModel = new Series($pdo);
-$series = $seriesModel->findById($series_id);
-
-if (!$series) {
- http_response_code(404);
- echo "Серия не найдена
";
- include 'views/footer.php';
- exit;
-}
-
-// Получаем только опубликованные книги серии
-$books = $seriesModel->getBooksInSeries($series_id, true);
-
-// Получаем информацию об авторе
-$stmt = $pdo->prepare("SELECT id, username, display_name FROM users WHERE id = ?");
-$stmt->execute([$series['user_id']]);
-$author = $stmt->fetch(PDO::FETCH_ASSOC);
-
-// Получаем статистику по опубликованным книгам
-$bookModel = new Book($pdo);
-$total_words = 0;
-$total_chapters = 0;
-
-foreach ($books as $book) {
- $book_stats = $bookModel->getBookStats($book['id'], true); // true - только опубликованные главы
- $total_words += $book_stats['total_words'] ?? 0;
- $total_chapters += $book_stats['chapter_count'] ?? 0;
-}
-
-$page_title = $series['title'] . ' — серия книг';
-include 'views/header.php';
-?>
-
-
-
-
- = e($series['title']) ?>
-
- Серия книг от
- = e($author['display_name'] ?: $author['username']) ?>
-
-
-
-
- = $Parsedown->text($series['description']) ?>
-
-
-
-
- Книг: = count($books) ?>
- Глав: = $total_chapters ?>
- Слов: = $total_words ?>
-
-
-
-
-
- В этой серии пока нет опубликованных книг
- Автор еще не опубликовал книги из этой серии
-
-
-
- Книги серии
-
-
-
-
-
-
-
-
-
- 📚
-
-
-
-
-
-
- Книга = $book['sort_order_in_series'] ?>
-
- = e($book['title']) ?>
-
-
-
- = e($book['genre']) ?>
-
-
-
- = nl2br(e($book['description'])) ?>
-
-
-
-
- Читать
-
-
- getBookStats($book['id'], true); // true - только опубликованные главы
- ?>
-
-
- Глав: = $book_stats['chapter_count'] ?? 0 ?> | Слов: = $book_stats['total_words'] ?? 0 ?>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+Неверный запрос";
+ include 'views/footer.php';
+ exit;
+}
+
+$seriesModel = new Series($pdo);
+$series = $seriesModel->findById($series_id);
+
+if (!$series) {
+ http_response_code(404);
+ echo "Серия не найдена
";
+ include 'views/footer.php';
+ exit;
+}
+
+// Получаем только опубликованные книги серии
+$books = $seriesModel->getBooksInSeries($series_id, true);
+
+// Получаем информацию об авторе
+$stmt = $pdo->prepare("SELECT id, username, display_name FROM users WHERE id = ?");
+$stmt->execute([$series['user_id']]);
+$author = $stmt->fetch(PDO::FETCH_ASSOC);
+
+// Получаем статистику по опубликованным книгам
+$bookModel = new Book($pdo);
+$total_words = 0;
+$total_chapters = 0;
+
+foreach ($books as $book) {
+ $book_stats = $bookModel->getBookStats($book['id'], true); // true - только опубликованные главы
+ $total_words += $book_stats['total_words'] ?? 0;
+ $total_chapters += $book_stats['chapter_count'] ?? 0;
+}
+
+$page_title = $series['title'] . ' — серия книг';
+include 'views/header.php';
+?>
+
+
+
+
+ = e($series['title']) ?>
+
+ Серия книг от
+ = e($author['display_name'] ?: $author['username']) ?>
+
+
+
+
+ = $Parsedown->text($series['description']) ?>
+
+
+
+
+ Книг: = count($books) ?>
+ Глав: = $total_chapters ?>
+ Слов: = $total_words ?>
+
+
+
+
+
+ В этой серии пока нет опубликованных книг
+ Автор еще не опубликовал книги из этой серии
+
+
+
+ Книги серии
+
+
+
+
+
+
+
+
+
+ 📚
+
+
+
+
+
+
+ Книга = $book['sort_order_in_series'] ?>
+
+ = e($book['title']) ?>
+
+
+
+ = e($book['genre']) ?>
+
+
+
+ = nl2br(e($book['description'])) ?>
+
+
+
+
+ Читать
+
+
+ getBookStats($book['id'], true); // true - только опубликованные главы
+ ?>
+
+
+ Глав: = $book_stats['chapter_count'] ?? 0 ?> | Слов: = $book_stats['total_words'] ?? 0 ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/views/header.php b/views/header.php
index 7bc23dd..a0e1b5e 100755
--- a/views/header.php
+++ b/views/header.php
@@ -1,51 +1,64 @@
-
-
-
-
-
-
- = e(APP_NAME) ?> - = e($page_title ?? 'Платформа для писателей') ?>
-
-
-
-
-
-
-
\ No newline at end of file
+
+
+
+
+
+
+ = e(APP_NAME) ?> - = e($page_title ?? 'Платформа для писателей') ?>
+
+
+
+
+
+
+
+
+
+ = e($_SESSION['info']) ?>
+
+
+
+
+
+
+ = e($_SESSION['warning']) ?>
+
+
+
\ No newline at end of file