version 1.0
This commit is contained in:
parent
b904e92da9
commit
2f9fba663a
|
|
@ -1,4 +1,6 @@
|
|||
/* style.css */
|
||||
/* style.css - оптимизированная версия */
|
||||
|
||||
/* Базовые стили */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
|
@ -22,6 +24,18 @@ input:not([type="checkbox"], [type="radio"]), select {
|
|||
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;
|
||||
|
|
@ -40,6 +54,7 @@ input:not([type="checkbox"], [type="radio"]), select {
|
|||
border: 1px solid #c8e6c9;
|
||||
}
|
||||
|
||||
/* Кнопки */
|
||||
.compact-button {
|
||||
padding: 3px 8px !important;
|
||||
font-size: 0.85rem;
|
||||
|
|
@ -62,23 +77,6 @@ input:not([type="checkbox"], [type="radio"]), select {
|
|||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
|
|
@ -181,23 +179,66 @@ input:not([type="checkbox"], [type="radio"]), select {
|
|||
color: var(--primary-inverse);
|
||||
}
|
||||
|
||||
@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 {
|
||||
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;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.adaptive-button {
|
||||
flex: 1 1 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
.action-button.primary {
|
||||
background: #007bff;
|
||||
border-color: #007bff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Стили для Markdown редактора */
|
||||
.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;
|
||||
|
|
@ -220,9 +261,31 @@ input:not([type="checkbox"], [type="radio"]), select {
|
|||
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;
|
||||
|
|
@ -256,11 +319,9 @@ input:not([type="checkbox"], [type="radio"]), select {
|
|||
box-shadow: 0 4px 8px rgba(0,0,0,0.3) !important;
|
||||
}
|
||||
|
||||
/* Стили для полноэкранных кнопок */
|
||||
/* Полноэкранный режим редактора */
|
||||
#fullscreen-controls {
|
||||
position: fixed !important;
|
||||
top: 15px !important;
|
||||
right: 15px !important;
|
||||
z-index: 9999 !important;
|
||||
display: flex !important;
|
||||
gap: 5px !important;
|
||||
|
|
@ -288,135 +349,7 @@ input:not([type="checkbox"], [type="radio"]), select {
|
|||
box-shadow: 0 4px 8px rgba(0,0,0,0.4) !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;
|
||||
}
|
||||
|
||||
/* Улучшенные стили для кнопок в полноэкранном режиме */
|
||||
#fullscreen-controls {
|
||||
position: fixed !important;
|
||||
z-index: 9999 !important;
|
||||
display: flex !important;
|
||||
gap: 5px !important;
|
||||
}
|
||||
|
||||
/* Для мобильных устройств */
|
||||
@media (max-width: 768px) {
|
||||
#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;
|
||||
}
|
||||
|
||||
/* Улучшенный полноэкранный режим для мобильных с учетом клавиатуры */
|
||||
#content.mobile-fullscreen {
|
||||
padding: 15px !important;
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
/* Дополнительные стили для очень маленьких экранов */
|
||||
@media (max-width: 480px) {
|
||||
#fullscreen-controls button {
|
||||
width: 55px !important;
|
||||
height: 55px !important;
|
||||
font-size: 22px !important;
|
||||
}
|
||||
|
||||
#content.mobile-fullscreen {
|
||||
padding: 10px !important;
|
||||
font-size: 16px !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::-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;
|
||||
}
|
||||
|
||||
/* Для Firefox */
|
||||
#content {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #c1c1c1 #f1f1f1;
|
||||
}
|
||||
|
||||
|
||||
/* Стили для отображения контента книг */
|
||||
.book-content {
|
||||
line-height: 1.7;
|
||||
font-family: Georgia, serif;
|
||||
|
|
@ -515,109 +448,13 @@ input:not([type="checkbox"], [type="radio"]), select {
|
|||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
/* Стили для диалогов */
|
||||
.dialogue {
|
||||
margin-left: 2rem;
|
||||
font-style: italic;
|
||||
color: #2c5aa0;
|
||||
}
|
||||
|
||||
/* Адаптивность для мобильных */
|
||||
@media (max-width: 768px) {
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
/* Стили для кнопок действий */
|
||||
.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;
|
||||
}
|
||||
|
||||
/* Адаптивность для мобильных */
|
||||
@media (max-width: 768px) {
|
||||
.action-button {
|
||||
padding: 0.6rem 1rem;
|
||||
font-size: 0.85rem;
|
||||
min-width: 120px;
|
||||
height: 42px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.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;
|
||||
}
|
||||
}
|
||||
/* assets/css/style.css - добавить */
|
||||
/* Обложки и медиа */
|
||||
.book-cover {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
|
@ -639,17 +476,276 @@ input:not([type="checkbox"], [type="radio"]), select {
|
|||
margin: 0 auto 1rem;
|
||||
}
|
||||
|
||||
/* Стили для страницы автора */
|
||||
.author-books .book-cover {
|
||||
transition: transform 0.3s ease;
|
||||
/* Аватарки и профиль */
|
||||
.avatar-container {
|
||||
text-align: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.author-books .book-cover:hover {
|
||||
transform: scale(1.05);
|
||||
.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;
|
||||
}
|
||||
|
|
@ -657,4 +753,175 @@ input:not([type="checkbox"], [type="radio"]), select {
|
|||
.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;
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
<?php
|
||||
require_once 'config/config.php';
|
||||
require_once 'models/Book.php';
|
||||
require_once 'includes/parsedown/ParsedownExtra.php';
|
||||
|
||||
$Parsedown = new ParsedownExtra();
|
||||
|
||||
$author_id = (int)($_GET['id'] ?? 0);
|
||||
if (!$author_id) {
|
||||
|
|
@ -10,7 +12,7 @@ if (!$author_id) {
|
|||
exit;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("SELECT id, username, display_name FROM users WHERE id = ?");
|
||||
$stmt = $pdo->prepare("SELECT id, username, display_name, avatar, bio FROM users WHERE id = ?");
|
||||
$stmt->execute([$author_id]);
|
||||
$author = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
|
|
@ -24,22 +26,79 @@ if (!$author) {
|
|||
$bookModel = new Book($pdo);
|
||||
$books = $bookModel->findByUser($author_id, true); // только опубликованные
|
||||
|
||||
// Получаем статистику автора
|
||||
$total_books = count($books);
|
||||
$total_words = 0;
|
||||
$total_chapters = 0;
|
||||
|
||||
foreach ($books as $book) {
|
||||
$book_stats = $bookModel->getBookStats($book['id'], true);
|
||||
$total_words += $book_stats['total_words'] ?? 0;
|
||||
$total_chapters += $book_stats['chapter_count'] ?? 0;
|
||||
}
|
||||
|
||||
$page_title = ($author['display_name'] ?: $author['username']) . ' — публичная страница';
|
||||
include 'views/header.php';
|
||||
?>
|
||||
|
||||
<h1><?= e($author['display_name'] ?: $author['username']) ?></h1>
|
||||
<div class="container">
|
||||
<article style="max-width: 800px; margin: 0 auto;">
|
||||
<header style="text-align: center; margin-bottom: 2rem; border-bottom: 2px solid #eee; padding-bottom: 1rem;">
|
||||
<!-- Аватарка автора -->
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<?php if (!empty($author['avatar'])): ?>
|
||||
<img src="<?= AVATARS_URL . e($author['avatar']) ?>"
|
||||
alt="<?= e($author['display_name'] ?: $author['username']) ?>"
|
||||
style="width: 150px; height: 150px; border-radius: 50%; border: 3px solid #007bff; object-fit: cover;"
|
||||
onerror="this.style.display='none'">
|
||||
<?php else: ?>
|
||||
<div style="width: 150px; height: 150px; border-radius: 50%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; align-items: center; justify-content: center; color: white; font-size: 3rem; margin: 0 auto;">
|
||||
<?= mb_substr(e($author['display_name'] ?: $author['username']), 0, 1) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if (empty($books)): ?>
|
||||
<p>У этого автора пока нет опубликованных книг.</p>
|
||||
<?php else: ?>
|
||||
<div class="grid">
|
||||
<?php foreach ($books as $b): ?>
|
||||
<article style="display: flex; gap: 1rem; align-items: flex-start;">
|
||||
<?php if ($b['cover_image']): ?>
|
||||
<h1 style="margin-bottom: 0.5rem;"><?= e($author['display_name'] ?: $author['username']) ?></h1>
|
||||
|
||||
<!-- Биография автора -->
|
||||
<?php if (!empty($author['bio'])): ?>
|
||||
<div style="background: #f8f9fa; padding: 1.5rem; border-radius: 8px; margin: 1rem 0; text-align: left;">
|
||||
<?= $Parsedown->text($author['bio']) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Статистика автора -->
|
||||
<div style="display: flex; justify-content: center; gap: 2rem; flex-wrap: wrap; font-size: 0.9em; color: #666;">
|
||||
<div style="text-align: center;">
|
||||
<div style="font-size: 1.5em; font-weight: bold; color: #007bff;"><?= $total_books ?></div>
|
||||
<div>Книг</div>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<div style="font-size: 1.5em; font-weight: bold; color: #28a745;"><?= $total_chapters ?></div>
|
||||
<div>Глав</div>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<div style="font-size: 1.5em; font-weight: bold; color: #6f42c1;"><?= $total_words ?></div>
|
||||
<div>Слов</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<h2 style="text-align: center; margin-bottom: 2rem;">Публикации автора</h2>
|
||||
|
||||
<?php if (empty($books)): ?>
|
||||
<div style="text-align: center; padding: 3rem; background: #f9f9f9; border-radius: 5px;">
|
||||
<h3>У этого автора пока нет опубликованных книг</h3>
|
||||
<p>Следите за обновлениями, скоро здесь появятся новые произведения!</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="author-books">
|
||||
<?php foreach ($books as $book): ?>
|
||||
<article style="display: flex; gap: 1rem; align-items: flex-start; margin-bottom: 2rem; padding: 1.5rem; background: #f8f9fa; border-radius: 8px;">
|
||||
<?php if ($book['cover_image']): ?>
|
||||
<div style="flex-shrink: 0;">
|
||||
<img src="<?= COVERS_URL . e($b['cover_image']) ?>"
|
||||
alt="<?= e($b['title']) ?>"
|
||||
<img src="<?= COVERS_URL . e($book['cover_image']) ?>"
|
||||
alt="<?= e($book['title']) ?>"
|
||||
style="max-width: 120px; height: auto; border-radius: 4px; border: 1px solid #ddd;"
|
||||
onerror="this.style.display='none'">
|
||||
</div>
|
||||
|
|
@ -50,18 +109,84 @@ include 'views/header.php';
|
|||
<?php endif; ?>
|
||||
|
||||
<div style="flex: 1;">
|
||||
<h3 style="margin-top: 0;"><?= e($b['title']) ?></h3>
|
||||
<?php if ($b['genre']): ?>
|
||||
<p style="color: #666; margin: 0.5rem 0;"><em><?= e($b['genre']) ?></em></p>
|
||||
<h3 style="margin-top: 0;"><?= e($book['title']) ?></h3>
|
||||
|
||||
<?php if ($book['genre']): ?>
|
||||
<p style="color: #666; margin: 0.5rem 0;"><em><?= e($book['genre']) ?></em></p>
|
||||
<?php endif; ?>
|
||||
<?php if ($b['description']): ?>
|
||||
<p style="margin-bottom: 1rem;"><?= nl2br(e($b['description'])) ?></p>
|
||||
|
||||
<?php if ($book['description']): ?>
|
||||
<p style="margin-bottom: 1rem;"><?= nl2br(e($book['description'])) ?></p>
|
||||
<?php endif; ?>
|
||||
<a href="view_book.php?share_token=<?= e($b['share_token']) ?>" class="adaptive-button">Читать</a>
|
||||
|
||||
<?php
|
||||
$book_stats = $bookModel->getBookStats($book['id'], true);
|
||||
$chapter_count = $book_stats['chapter_count'] ?? 0;
|
||||
$word_count = $book_stats['total_words'] ?? 0;
|
||||
?>
|
||||
|
||||
<div style="display: flex; gap: 10px; flex-wrap: wrap; align-items: center;">
|
||||
<a href="view_book.php?share_token=<?= e($book['share_token']) ?>" class="adaptive-button">
|
||||
Читать книгу
|
||||
</a>
|
||||
|
||||
<small style="color: #666;">
|
||||
Глав: <?= $chapter_count ?> | Слов: <?= $word_count ?>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<footer style="margin-top: 3rem; padding-top: 1rem; border-top: 2px solid #eee; text-align: center;">
|
||||
<p style="color: #666;">
|
||||
Страница автора создана в <?= e(APP_NAME) ?> •
|
||||
<?= date('Y') ?>
|
||||
</p>
|
||||
</footer>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.author-books article {
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.author-books article:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.author-books article {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.author-books .book-cover {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
header .author-stats {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<?php include 'views/footer.php'; ?>
|
||||
|
|
@ -36,11 +36,22 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||
if (empty($title)) {
|
||||
$_SESSION['error'] = "Название книги обязательно";
|
||||
} else {
|
||||
$series_id = !empty($_POST['series_id']) ? (int)$_POST['series_id'] : null;
|
||||
$sort_order_in_series = !empty($_POST['sort_order_in_series']) ? (int)$_POST['sort_order_in_series'] : null;
|
||||
|
||||
// Если серия указана, но порядок нет - генерируем автоматически
|
||||
if ($series_id && !$sort_order_in_series) {
|
||||
$seriesModel = new Series($pdo);
|
||||
$sort_order_in_series = $seriesModel->getNextSortOrder($series_id);
|
||||
}
|
||||
|
||||
$data = [
|
||||
'title' => $title,
|
||||
'description' => $description,
|
||||
'genre' => $genre,
|
||||
'user_id' => $user_id
|
||||
'user_id' => $user_id,
|
||||
'series_id' => $series_id,
|
||||
'sort_order_in_series' => $sort_order_in_series
|
||||
];
|
||||
$data['published'] = isset($_POST['published']) ? 1 : 0;
|
||||
|
||||
|
|
@ -110,7 +121,32 @@ include 'views/header.php';
|
|||
value="<?= e($book['genre'] ?? $_POST['genre'] ?? '') ?>"
|
||||
placeholder="Например: Фантастика, Роман, Детектив..."
|
||||
style="width: 100%; margin-bottom: 1.5rem;">
|
||||
<label for="series_id" style="display: block; margin-bottom: 0.5rem; font-weight: bold;">
|
||||
Серия
|
||||
</label>
|
||||
<select id="series_id" name="series_id" style="width: 100%; margin-bottom: 1rem;">
|
||||
<option value="">-- Без серии --</option>
|
||||
<?php
|
||||
$seriesModel = new Series($pdo);
|
||||
$user_series = $seriesModel->findByUser($user_id, false);
|
||||
|
||||
foreach ($user_series as $ser):
|
||||
$selected = ($ser['id'] == ($book['series_id'] ?? 0)) ? 'selected' : '';
|
||||
?>
|
||||
<option value="<?= $ser['id'] ?>" <?= $selected ?>>
|
||||
<?= e($ser['title']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
|
||||
<label for="sort_order_in_series" style="display: block; margin-bottom: 0.5rem; font-weight: bold;">
|
||||
Порядок в серии
|
||||
</label>
|
||||
<input type="number" id="sort_order_in_series" name="sort_order_in_series"
|
||||
value="<?= e($book['sort_order_in_series'] ?? '') ?>"
|
||||
placeholder="Номер по порядку в серии"
|
||||
min="1"
|
||||
style="width: 100%; margin-bottom: 1.5rem;">
|
||||
<!-- ПОЛЕ ДЛЯ ОБЛОЖКИ -->
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
<label for="cover_image" style="display: block; margin-bottom: 0.5rem; font-weight: bold;">
|
||||
|
|
|
|||
17
books.php
17
books.php
|
|
@ -57,6 +57,23 @@ include 'views/header.php';
|
|||
<?php endif; ?>
|
||||
<header>
|
||||
<h3><?= e($book['title']) ?>
|
||||
<?php if ($book['series_id']): ?>
|
||||
<?php
|
||||
$series_stmt = $pdo->prepare("SELECT title FROM series WHERE id = ?");
|
||||
$series_stmt->execute([$book['series_id']]);
|
||||
$series_title = $series_stmt->fetch()['title'] ?? '';
|
||||
?>
|
||||
<?php if ($series_title): ?>
|
||||
<div style="margin: 0.3rem 0;">
|
||||
<small style="color: #007bff;">
|
||||
📚 Серия: <?= e($series_title) ?>
|
||||
<?php if ($book['sort_order_in_series']): ?>
|
||||
(Книга <?= $book['sort_order_in_series'] ?>)
|
||||
<?php endif; ?>
|
||||
</small>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
<div style="display: flex; gap: 3px; float:right;">
|
||||
<a href="book_edit.php?id=<?= $book['id'] ?>" class="compact-button secondary" title="Редактировать книгу">
|
||||
✏️
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<?php
|
||||
// config/config.php
|
||||
// config/config.php - автоматически сгенерирован установщиком
|
||||
// Подключаем функции
|
||||
require_once __DIR__ . '/../includes/functions.php';
|
||||
session_start();
|
||||
|
|
@ -10,17 +10,22 @@ define('DB_USER', 'writer_mirv');
|
|||
define('DB_PASS', 'writer_moloko22');
|
||||
define('DB_NAME', 'writer_app');
|
||||
define('SITE_URL', 'https://writer.mirv.top');
|
||||
//define('BASE_URL', 'http://' . $_SERVER['HTTP_HOST'] . '/'); // Измените на ваш домен
|
||||
|
||||
// Настройки приложения
|
||||
define('APP_NAME', 'Web Writer');
|
||||
define('UPLOAD_PATH', __DIR__ . '/../uploads/');
|
||||
define('COVERS_PATH', UPLOAD_PATH . 'covers/');
|
||||
define('COVERS_URL', SITE_URL . '/uploads/covers/');
|
||||
define('AVATARS_PATH', UPLOAD_PATH . 'avatars/');
|
||||
define('AVATARS_URL', SITE_URL . '/uploads/avatars/');
|
||||
|
||||
// Создаем папку для загрузок, если ее нет
|
||||
// if (!file_exists(COVERS_PATH)) {
|
||||
// mkdir(COVERS_PATH, 0765, true);
|
||||
// }
|
||||
if (!file_exists(COVERS_PATH)) {
|
||||
mkdir(COVERS_PATH, 0755, true);
|
||||
}
|
||||
if (!file_exists(AVATARS_PATH)) {
|
||||
mkdir(AVATARS_PATH, 0755, true);
|
||||
}
|
||||
|
||||
// Подключение к базе данных
|
||||
try {
|
||||
|
|
@ -31,7 +36,6 @@ try {
|
|||
die("Ошибка подключения к базе данных");
|
||||
}
|
||||
|
||||
|
||||
// Автозагрузка моделей
|
||||
spl_autoload_register(function ($class_name) {
|
||||
$model_file = __DIR__ . '/../models/' . $class_name . '.php';
|
||||
|
|
|
|||
165
dashboard.php
165
dashboard.php
|
|
@ -4,7 +4,33 @@ require_login();
|
|||
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$bookModel = new Book($pdo);
|
||||
$seriesModel = new Series($pdo);
|
||||
|
||||
$books = $bookModel->findByUser($user_id);
|
||||
$series = $seriesModel->findByUser($user_id);
|
||||
|
||||
// Статистика по книгам
|
||||
$total_chapters = 0;
|
||||
$total_words = 0;
|
||||
foreach ($books as $book) {
|
||||
$total_chapters += $book['chapter_count'];
|
||||
$total_words += $book['total_words'];
|
||||
}
|
||||
|
||||
// Статистика по сериям
|
||||
$series_stats = [
|
||||
'total_series' => count($series),
|
||||
'series_with_books' => 0,
|
||||
'total_books_in_series' => 0
|
||||
];
|
||||
|
||||
foreach ($series as $ser) {
|
||||
$series_books = $seriesModel->getBooksInSeries($ser['id']);
|
||||
$series_stats['total_books_in_series'] += count($series_books);
|
||||
if (count($series_books) > 0) {
|
||||
$series_stats['series_with_books']++;
|
||||
}
|
||||
}
|
||||
|
||||
$page_title = "Панель управления";
|
||||
include 'views/header.php';
|
||||
|
|
@ -12,49 +38,152 @@ include 'views/header.php';
|
|||
|
||||
<h1>Добро пожаловать, <?= e($_SESSION['display_name']) ?>!</h1>
|
||||
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<a href="profile.php" class="adaptive-button secondary">✏️ Редактировать профиль</a>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<article>
|
||||
<h2>📚 Мои книги</h2>
|
||||
<p>Управляйте вашими книгами и главами</p>
|
||||
<a href="books.php" role="button">
|
||||
<div class="dashboard-buttons">
|
||||
<a href="books.php" role="button" class="dashboard-button">
|
||||
Мои книги (<?= count($books) ?>)
|
||||
</a>
|
||||
|
||||
<a href="book_edit.php" role="button">➕ Новая книга</a>
|
||||
<a href="book_edit.php" role="button" class="dashboard-button new">
|
||||
➕ Новая книга
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<h2>📊 Статистика</h2>
|
||||
<?php
|
||||
$total_chapters = 0;
|
||||
$total_words = 0;
|
||||
foreach ($books as $book) {
|
||||
$total_chapters += $book['chapter_count'];
|
||||
$total_words += $book['total_words'];
|
||||
}
|
||||
?>
|
||||
<div class="stats-list">
|
||||
<p><strong>Книг:</strong> <?= count($books) ?></p>
|
||||
<p><strong>Глав:</strong> <?= $total_chapters ?></p>
|
||||
<p><strong>Всего слов:</strong> <?= $total_words ?></p>
|
||||
<?php if ($total_words > 0): ?>
|
||||
<p><strong>Средняя глава:</strong> <?= round($total_words / max(1, $total_chapters)) ?> слов</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<h2>📖 Мои серии</h2>
|
||||
<p>Управляйте сериями книг</p>
|
||||
<div class="dashboard-buttons">
|
||||
<a href="series.php" role="button" class="dashboard-button">
|
||||
Мои серии (<?= $series_stats['total_series'] ?>)
|
||||
</a>
|
||||
<a href="series_edit.php" role="button" class="dashboard-button new">
|
||||
➕ Новая серия
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<?php if ($series_stats['total_series'] > 0): ?>
|
||||
<div class="series-stats">
|
||||
<p><strong>Книг в сериях:</strong> <?= $series_stats['total_books_in_series'] ?></p>
|
||||
<p><strong>Заполненных серий:</strong> <?= $series_stats['series_with_books'] ?></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($books)): ?>
|
||||
<div style="margin-top: 2rem;">
|
||||
|
||||
<div class="grid">
|
||||
<article>
|
||||
<div class="dashboard-section">
|
||||
<h2>Недавние книги</h2>
|
||||
<div class="grid">
|
||||
<?php foreach (array_slice($books, 0, 3) as $book): ?>
|
||||
<article>
|
||||
<h4><?= e($book['title']) ?></h4>
|
||||
<article class="dashboard-item">
|
||||
<h4>
|
||||
<?= e($book['title']) ?>
|
||||
<?php if ($book['series_id']): ?>
|
||||
<?php
|
||||
$series_stmt = $pdo->prepare("SELECT title FROM series WHERE id = ?");
|
||||
$series_stmt->execute([$book['series_id']]);
|
||||
$series_title = $series_stmt->fetch()['title'] ?? '';
|
||||
?>
|
||||
<?php if ($series_title): ?>
|
||||
<br><small style="color: #007bff;">📚 <?= e($series_title) ?></small>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</h4>
|
||||
<p>Глав: <?= $book['chapter_count'] ?> | Слов: <?= $book['total_words'] ?></p>
|
||||
<a href="book_edit.php?id=<?= $book['id'] ?>" role="button" class="secondary">
|
||||
<div class="action-buttons">
|
||||
<a href="book_edit.php?id=<?= $book['id'] ?>" role="button" class="compact-button secondary">
|
||||
Редактировать
|
||||
</a>
|
||||
<a href="chapters.php?book_id=<?= $book['id'] ?>" role="button" class="compact-button secondary">
|
||||
Главы
|
||||
</a>
|
||||
<a href="view_book.php?share_token=<?= $book['share_token'] ?>" role="button" class="compact-button secondary" target="_blank">
|
||||
Просмотр
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<?php if (count($books) > 3): ?>
|
||||
<div style="text-align: center; margin-top: 1rem;">
|
||||
<a href="books.php" role="button" class="secondary">📚 Показать все книги (<?= count($books) ?>)</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($series)): ?>
|
||||
<div class="dashboard-section">
|
||||
<h2>Недавние серии</h2>
|
||||
<div class="grid">
|
||||
<?php foreach (array_slice($series, 0, 3) as $ser): ?>
|
||||
<article class="dashboard-item">
|
||||
<h4><?= e($ser['title']) ?></h4>
|
||||
|
||||
<?php
|
||||
$books_in_series = $seriesModel->getBooksInSeries($ser['id']);
|
||||
$series_words = 0;
|
||||
$series_chapters = 0;
|
||||
|
||||
foreach ($books_in_series as $book) {
|
||||
$book_stats = $bookModel->getBookStats($book['id']);
|
||||
$series_words += $book_stats['total_words'] ?? 0;
|
||||
$series_chapters += $book_stats['chapter_count'] ?? 0;
|
||||
}
|
||||
?>
|
||||
|
||||
<p>Книг: <?= count($books_in_series) ?> | Глав: <?= $series_chapters ?> | Слов: <?= $series_words ?></p>
|
||||
|
||||
<div class="action-buttons">
|
||||
<a href="series_edit.php?id=<?= $ser['id'] ?>" role="button" class="compact-button secondary">
|
||||
Редактировать
|
||||
</a>
|
||||
<a href="view_series.php?id=<?= $ser['id'] ?>" role="button" class="compact-button secondary" target="_blank">
|
||||
Просмотр
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<?php if (count($series) > 3): ?>
|
||||
<div style="text-align: center; margin-top: 1rem;">
|
||||
<a href="series.php" role="button" class="secondary">📖 Показать все серии (<?= count($series) ?>)</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (empty($books) && empty($series)): ?>
|
||||
<div class="welcome-message">
|
||||
<h3>Добро пожаловать в <?= e(APP_NAME) ?>!</h3>
|
||||
<p>Начните создавать свои литературные произведения</p>
|
||||
<div class="welcome-buttons">
|
||||
<a href="book_edit.php" role="button" class="contrast">📖 Создать первую книгу</a>
|
||||
<a href="series_edit.php" role="button" class="secondary">📚 Создать первую серию</a>
|
||||
</div>
|
||||
<div style="margin-top: 1.5rem;">
|
||||
<a href="profile.php" role="button" class="secondary">✏️ Настроить профиль</a>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
|
|
|||
|
|
@ -189,4 +189,170 @@ function optimizeImage($file_path) {
|
|||
imagedestroy($new_image);
|
||||
}
|
||||
}
|
||||
|
||||
// В includes/functions.php, после функции handleCoverUpload
|
||||
|
||||
function handleAvatarUpload($file, $user_id) {
|
||||
global $pdo;
|
||||
|
||||
// Проверяем папку для загрузок
|
||||
if (!file_exists(AVATARS_PATH)) {
|
||||
mkdir(AVATARS_PATH, 0755, true);
|
||||
}
|
||||
|
||||
$allowed_types = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'];
|
||||
$max_size = 2 * 1024 * 1024; // 2MB
|
||||
|
||||
// Проверка типа файла
|
||||
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||
$mime_type = finfo_file($finfo, $file['tmp_name']);
|
||||
finfo_close($finfo);
|
||||
|
||||
if (!in_array($mime_type, $allowed_types)) {
|
||||
return ['success' => false, 'error' => 'Разрешены только JPG, PNG, GIF и WebP изображения'];
|
||||
}
|
||||
|
||||
// Проверка размера
|
||||
if ($file['size'] > $max_size) {
|
||||
return ['success' => false, 'error' => 'Размер изображения не должен превышать 2MB'];
|
||||
}
|
||||
|
||||
// Проверка на ошибки загрузки
|
||||
if ($file['error'] !== UPLOAD_ERR_OK) {
|
||||
return ['success' => false, 'error' => 'Ошибка загрузки файла: ' . $file['error']];
|
||||
}
|
||||
|
||||
// Проверка реального типа файла по содержимому
|
||||
$allowed_signatures = [
|
||||
'image/jpeg' => "\xFF\xD8\xFF",
|
||||
'image/png' => "\x89\x50\x4E\x47",
|
||||
'image/gif' => "GIF",
|
||||
'image/webp' => "RIFF"
|
||||
];
|
||||
|
||||
$file_content = file_get_contents($file['tmp_name']);
|
||||
$signature = substr($file_content, 0, 4);
|
||||
|
||||
$valid_signature = false;
|
||||
foreach ($allowed_signatures as $type => $sig) {
|
||||
if (strpos($signature, $sig) === 0) {
|
||||
$valid_signature = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$valid_signature) {
|
||||
return ['success' => false, 'error' => 'Неверный формат изображения'];
|
||||
}
|
||||
|
||||
// Генерация уникального имени файла
|
||||
$extension = pathinfo($file['name'], PATHINFO_EXTENSION);
|
||||
$filename = 'avatar_' . $user_id . '_' . time() . '.' . $extension;
|
||||
$file_path = AVATARS_PATH . $filename;
|
||||
|
||||
// Удаляем старый аватар если есть
|
||||
$userModel = new User($pdo);
|
||||
$user = $userModel->findById($user_id);
|
||||
if (!empty($user['avatar'])) {
|
||||
$old_file_path = AVATARS_PATH . $user['avatar'];
|
||||
if (file_exists($old_file_path)) {
|
||||
unlink($old_file_path);
|
||||
}
|
||||
}
|
||||
|
||||
// Сохраняем новую аватарку
|
||||
if (move_uploaded_file($file['tmp_name'], $file_path)) {
|
||||
// Оптимизируем изображение
|
||||
optimizeAvatar($file_path);
|
||||
return ['success' => true, 'filename' => $filename];
|
||||
} else {
|
||||
return ['success' => false, 'error' => 'Не удалось сохранить файл'];
|
||||
}
|
||||
}
|
||||
|
||||
function optimizeAvatar($file_path) {
|
||||
// Оптимизация аватарки - ресайз до 200x200
|
||||
list($width, $height, $type) = getimagesize($file_path);
|
||||
|
||||
$max_size = 200;
|
||||
|
||||
if ($width > $max_size || $height > $max_size) {
|
||||
// Вычисляем новые размеры
|
||||
$ratio = $width / $height;
|
||||
if ($ratio > 1) {
|
||||
$new_width = $max_size;
|
||||
$new_height = $max_size / $ratio;
|
||||
} else {
|
||||
$new_width = $max_size * $ratio;
|
||||
$new_height = $max_size;
|
||||
}
|
||||
|
||||
// Создаем новое изображение
|
||||
$new_image = imagecreatetruecolor($new_width, $new_height);
|
||||
|
||||
// Загружаем исходное изображение в зависимости от типа
|
||||
switch ($type) {
|
||||
case IMAGETYPE_JPEG:
|
||||
$source = imagecreatefromjpeg($file_path);
|
||||
break;
|
||||
case IMAGETYPE_PNG:
|
||||
$source = imagecreatefrompng($file_path);
|
||||
// Сохраняем прозрачность для PNG
|
||||
imagecolortransparent($new_image, imagecolorallocatealpha($new_image, 0, 0, 0, 127));
|
||||
imagealphablending($new_image, false);
|
||||
imagesavealpha($new_image, true);
|
||||
break;
|
||||
case IMAGETYPE_GIF:
|
||||
$source = imagecreatefromgif($file_path);
|
||||
break;
|
||||
case IMAGETYPE_WEBP:
|
||||
$source = imagecreatefromwebp($file_path);
|
||||
break;
|
||||
default:
|
||||
return; // Не поддерживаемый тип
|
||||
}
|
||||
|
||||
// Ресайз и сохраняем
|
||||
imagecopyresampled($new_image, $source, 0, 0, 0, 0, $new_width, $new_height, $width, $height);
|
||||
|
||||
switch ($type) {
|
||||
case IMAGETYPE_JPEG:
|
||||
imagejpeg($new_image, $file_path, 85);
|
||||
break;
|
||||
case IMAGETYPE_PNG:
|
||||
imagepng($new_image, $file_path, 8);
|
||||
break;
|
||||
case IMAGETYPE_GIF:
|
||||
imagegif($new_image, $file_path);
|
||||
break;
|
||||
case IMAGETYPE_WEBP:
|
||||
imagewebp($new_image, $file_path, 85);
|
||||
break;
|
||||
}
|
||||
|
||||
// Освобождаем память
|
||||
imagedestroy($source);
|
||||
imagedestroy($new_image);
|
||||
}
|
||||
}
|
||||
|
||||
function deleteUserAvatar($user_id) {
|
||||
global $pdo;
|
||||
|
||||
$userModel = new User($pdo);
|
||||
$user = $userModel->findById($user_id);
|
||||
|
||||
if (!empty($user['avatar'])) {
|
||||
$file_path = AVATARS_PATH . $user['avatar'];
|
||||
if (file_exists($file_path)) {
|
||||
unlink($file_path);
|
||||
}
|
||||
|
||||
// Обновляем запись в БД
|
||||
$stmt = $pdo->prepare("UPDATE users SET avatar = NULL WHERE id = ?");
|
||||
return $stmt->execute([$user_id]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
?>
|
||||
|
|
@ -0,0 +1,440 @@
|
|||
<?php
|
||||
// install.php - установщик приложения
|
||||
|
||||
// Проверяем, не установлено ли приложение уже
|
||||
if (file_exists('config/config.php')) {
|
||||
die('Приложение уже установлено. Для переустановки удалите файл config/config.php');
|
||||
}
|
||||
|
||||
// SQL для создания таблиц
|
||||
$database_sql = <<<SQL
|
||||
SET FOREIGN_KEY_CHECKS=0;
|
||||
|
||||
-- Удаляем существующие таблицы в правильном порядке (сначала дочерние, потом родительские)
|
||||
DROP TABLE IF EXISTS `user_sessions`;
|
||||
DROP TABLE IF EXISTS `chapters`;
|
||||
DROP TABLE IF EXISTS `books`;
|
||||
DROP TABLE IF EXISTS `series`;
|
||||
DROP TABLE IF EXISTS `users`;
|
||||
|
||||
-- Таблица пользователей
|
||||
CREATE TABLE `users` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`username` varchar(255) NOT NULL,
|
||||
`display_name` varchar(255) DEFAULT NULL,
|
||||
`password_hash` varchar(255) NOT NULL,
|
||||
`email` varchar(255) DEFAULT NULL,
|
||||
`avatar` varchar(500) DEFAULT NULL,
|
||||
`bio` text DEFAULT NULL,
|
||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||
`last_login` timestamp NULL DEFAULT NULL,
|
||||
`is_active` tinyint(1) DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `username` (`username`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
|
||||
-- Таблица серий
|
||||
CREATE TABLE `series` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`title` varchar(255) NOT NULL,
|
||||
`description` text DEFAULT NULL,
|
||||
`user_id` int(11) NOT NULL,
|
||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||
`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `user_id` (`user_id`),
|
||||
KEY `idx_series_user_id` (`user_id`),
|
||||
CONSTRAINT `series_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
|
||||
-- Таблица книг
|
||||
CREATE TABLE `books` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`title` varchar(255) NOT NULL,
|
||||
`description` text DEFAULT NULL,
|
||||
`genre` varchar(100) DEFAULT NULL,
|
||||
`cover_image` varchar(500) DEFAULT NULL,
|
||||
`user_id` int(11) NOT NULL,
|
||||
`series_id` int(11) DEFAULT NULL,
|
||||
`sort_order_in_series` int(11) DEFAULT NULL,
|
||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||
`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||
`share_token` varchar(32) DEFAULT NULL,
|
||||
`published` tinyint(1) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `share_token` (`share_token`),
|
||||
KEY `user_id` (`user_id`),
|
||||
KEY `series_id` (`series_id`),
|
||||
KEY `idx_sort_order_in_series` (`sort_order_in_series`),
|
||||
KEY `idx_books_series_id` (`series_id`),
|
||||
KEY `idx_books_sort_order` (`sort_order_in_series`),
|
||||
CONSTRAINT `books_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `books_ibfk_2` FOREIGN KEY (`series_id`) REFERENCES `series` (`id`) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
|
||||
-- Таблица глав
|
||||
CREATE TABLE `chapters` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`book_id` int(11) NOT NULL,
|
||||
`title` varchar(255) NOT NULL,
|
||||
`content` text NOT NULL,
|
||||
`word_count` int(11) DEFAULT 0,
|
||||
`sort_order` int(11) NOT NULL DEFAULT 0,
|
||||
`status` enum('draft','published') DEFAULT 'draft',
|
||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||
`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `book_id` (`book_id`),
|
||||
CONSTRAINT `chapters_ibfk_1` FOREIGN KEY (`book_id`) REFERENCES `books` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
|
||||
-- Таблица сессий пользователей
|
||||
CREATE TABLE `user_sessions` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int(11) NOT NULL,
|
||||
`session_token` varchar(64) NOT NULL,
|
||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||
`expires_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `user_id` (`user_id`),
|
||||
CONSTRAINT `user_sessions_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
|
||||
SET FOREIGN_KEY_CHECKS=1;
|
||||
SQL;
|
||||
|
||||
$step = $_GET['step'] ?? '1';
|
||||
$error = '';
|
||||
$success = '';
|
||||
|
||||
// Обработка формы
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if ($step === '1') {
|
||||
// Шаг 1: Проверка подключения к БД
|
||||
$db_host = $_POST['db_host'] ?? 'localhost';
|
||||
$db_name = $_POST['db_name'] ?? 'writer_app';
|
||||
$db_user = $_POST['db_user'] ?? '';
|
||||
$db_pass = $_POST['db_pass'] ?? '';
|
||||
|
||||
try {
|
||||
// Пытаемся подключиться к MySQL
|
||||
$pdo = new PDO("mysql:host=$db_host", $db_user, $db_pass);
|
||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
// Пытаемся создать базу данных если не существует
|
||||
$pdo->exec("CREATE DATABASE IF NOT EXISTS `$db_name` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
|
||||
$pdo->exec("USE `$db_name`");
|
||||
|
||||
// Сохраняем данные в сессии для следующего шага
|
||||
session_start();
|
||||
$_SESSION['install_data'] = [
|
||||
'db_host' => $db_host,
|
||||
'db_name' => $db_name,
|
||||
'db_user' => $db_user,
|
||||
'db_pass' => $db_pass
|
||||
];
|
||||
|
||||
header('Location: install.php?step=2');
|
||||
exit;
|
||||
|
||||
} catch (PDOException $e) {
|
||||
$error = "Ошибка подключения к базе данных: " . $e->getMessage();
|
||||
}
|
||||
|
||||
} elseif ($step === '2') {
|
||||
// Шаг 2: Создание администратора
|
||||
session_start();
|
||||
if (!isset($_SESSION['install_data'])) {
|
||||
header('Location: install.php?step=1');
|
||||
exit;
|
||||
}
|
||||
|
||||
$admin_username = $_POST['admin_username'] ?? '';
|
||||
$admin_password = $_POST['admin_password'] ?? '';
|
||||
$admin_email = $_POST['admin_email'] ?? '';
|
||||
$admin_display_name = $_POST['admin_display_name'] ?? $admin_username;
|
||||
|
||||
if (empty($admin_username) || empty($admin_password)) {
|
||||
$error = 'Имя пользователя и пароль администратора обязательны';
|
||||
} else {
|
||||
try {
|
||||
$db = $_SESSION['install_data'];
|
||||
$pdo = new PDO("mysql:host={$db['db_host']};dbname={$db['db_name']}", $db['db_user'], $db['db_pass']);
|
||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
// Создаем таблицы
|
||||
$pdo->exec($database_sql);
|
||||
|
||||
// Создаем администратора
|
||||
$password_hash = password_hash($admin_password, PASSWORD_DEFAULT);
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO users (username, display_name, password_hash, email, is_active, created_at)
|
||||
VALUES (?, ?, ?, ?, 1, NOW())
|
||||
");
|
||||
$stmt->execute([$admin_username, $admin_display_name, $password_hash, $admin_email]);
|
||||
|
||||
// Создаем config.php
|
||||
$config_content = generate_config($db);
|
||||
if (file_put_contents('config/config.php', $config_content)) {
|
||||
// Создаем папки для загрузок
|
||||
if (!file_exists('uploads/covers')) {
|
||||
mkdir('uploads/covers', 0755, true);
|
||||
}
|
||||
if (!file_exists('uploads/avatars')) {
|
||||
mkdir('uploads/avatars', 0755, true);
|
||||
}
|
||||
|
||||
$success = 'Установка завершена успешно!';
|
||||
session_destroy();
|
||||
} else {
|
||||
$error = 'Не удалось создать файл config.php. Проверьте права доступа к папке config/';
|
||||
}
|
||||
|
||||
} catch (PDOException $e) {
|
||||
$error = "Ошибка при установке: " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function generate_config($db) {
|
||||
$site_url = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://$_SERVER[HTTP_HOST]";
|
||||
$base_path = str_replace('/install.php', '', $_SERVER['PHP_SELF']);
|
||||
$site_url .= $base_path;
|
||||
|
||||
return <<<EOT
|
||||
<?php
|
||||
// config/config.php - автоматически сгенерирован установщиком
|
||||
// Подключаем функции
|
||||
require_once __DIR__ . '/../includes/functions.php';
|
||||
session_start();
|
||||
|
||||
// Настройки базы данных
|
||||
define('DB_HOST', '{$db['db_host']}');
|
||||
define('DB_USER', '{$db['db_user']}');
|
||||
define('DB_PASS', '{$db['db_pass']}');
|
||||
define('DB_NAME', '{$db['db_name']}');
|
||||
define('SITE_URL', '{$site_url}');
|
||||
|
||||
// Настройки приложения
|
||||
define('APP_NAME', 'Web Writer');
|
||||
define('UPLOAD_PATH', __DIR__ . '/../uploads/');
|
||||
define('COVERS_PATH', UPLOAD_PATH . 'covers/');
|
||||
define('COVERS_URL', SITE_URL . '/uploads/covers/');
|
||||
define('AVATARS_PATH', UPLOAD_PATH . 'avatars/');
|
||||
define('AVATARS_URL', SITE_URL . '/uploads/avatars/');
|
||||
|
||||
// Создаем папку для загрузок, если ее нет
|
||||
if (!file_exists(COVERS_PATH)) {
|
||||
mkdir(COVERS_PATH, 0755, true);
|
||||
}
|
||||
if (!file_exists(AVATARS_PATH)) {
|
||||
mkdir(AVATARS_PATH, 0755, true);
|
||||
}
|
||||
|
||||
// Подключение к базе данных
|
||||
try {
|
||||
\$pdo = new PDO("mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=utf8mb4", DB_USER, DB_PASS);
|
||||
\$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
} catch(PDOException \$e) {
|
||||
error_log("DB Error: " . \$e->getMessage());
|
||||
die("Ошибка подключения к базе данных");
|
||||
}
|
||||
|
||||
// Автозагрузка моделей
|
||||
spl_autoload_register(function (\$class_name) {
|
||||
\$model_file = __DIR__ . '/../models/' . \$class_name . '.php';
|
||||
if (file_exists(\$model_file)) {
|
||||
require_once \$model_file;
|
||||
}
|
||||
});
|
||||
?>
|
||||
EOT;
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Установка Web Writer</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@1.5.10/css/pico.min.css">
|
||||
<style>
|
||||
.installation-steps {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.step {
|
||||
padding: 10px 20px;
|
||||
margin: 0 10px;
|
||||
border-radius: 20px;
|
||||
background: #f0f0f0;
|
||||
color: #666;
|
||||
}
|
||||
.step.active {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
}
|
||||
.install-container {
|
||||
max-width: 600px;
|
||||
margin: 2rem auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.button-group a,
|
||||
.button-group button {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 0.75rem;
|
||||
text-decoration: none;
|
||||
border: 1px solid;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.button-group a {
|
||||
background: var(--secondary);
|
||||
border-color: var(--secondary);
|
||||
color: var(--secondary-inverse);
|
||||
}
|
||||
.button-group button {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: var(--primary-inverse);
|
||||
}
|
||||
.button-group a:hover,
|
||||
.button-group button:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="container">
|
||||
<div class="install-container">
|
||||
<h1 style="text-align: center;">Установка Web Writer</h1>
|
||||
|
||||
<!-- Шаги установки -->
|
||||
<div class="installation-steps">
|
||||
<div class="step <?= $step === '1' ? 'active' : '' ?>">1. База данных</div>
|
||||
<div class="step <?= $step === '2' ? 'active' : '' ?>">2. Администратор</div>
|
||||
<div class="step <?= $step === '3' ? 'active' : '' ?>">3. Завершение</div>
|
||||
</div>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-error">
|
||||
<?= htmlspecialchars($error) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($success): ?>
|
||||
<div class="alert alert-success">
|
||||
<?= htmlspecialchars($success) ?>
|
||||
<div style="margin-top: 1rem;">
|
||||
<a href="index.php" role="button" class="contrast">Перейти к приложению</a>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!$success): ?>
|
||||
<?php if ($step === '1'): ?>
|
||||
<!-- Шаг 1: Настройки базы данных -->
|
||||
<form method="post">
|
||||
<h3>Настройки базы данных</h3>
|
||||
|
||||
<label for="db_host">
|
||||
Хост БД
|
||||
<input type="text" id="db_host" name="db_host" value="<?= htmlspecialchars($_POST['db_host'] ?? 'localhost') ?>" required>
|
||||
</label>
|
||||
|
||||
<label for="db_name">
|
||||
Имя базы данных
|
||||
<input type="text" id="db_name" name="db_name" value="<?= htmlspecialchars($_POST['db_name'] ?? 'writer_app') ?>" required>
|
||||
</label>
|
||||
|
||||
<label for="db_user">
|
||||
Пользователь БД
|
||||
<input type="text" id="db_user" name="db_user" value="<?= htmlspecialchars($_POST['db_user'] ?? '') ?>" required>
|
||||
</label>
|
||||
|
||||
<label for="db_pass">
|
||||
Пароль БД
|
||||
<input type="password" id="db_pass" name="db_pass" value="<?= htmlspecialchars($_POST['db_pass'] ?? '') ?>">
|
||||
</label>
|
||||
|
||||
<button type="submit" class="contrast" style="width: 100%;">Продолжить</button>
|
||||
</form>
|
||||
|
||||
<?php elseif ($step === '2'): ?>
|
||||
<!-- Шаг 2: Создание администратора -->
|
||||
<form method="post">
|
||||
<h3>Создание администратора</h3>
|
||||
<p>Создайте учетную запись администратора для управления приложением.</p>
|
||||
|
||||
<label for="admin_username">
|
||||
Имя пользователя *
|
||||
<input type="text" id="admin_username" name="admin_username" value="<?= htmlspecialchars($_POST['admin_username'] ?? '') ?>" required>
|
||||
</label>
|
||||
|
||||
<label for="admin_password">
|
||||
Пароль *
|
||||
<input type="password" id="admin_password" name="admin_password" required>
|
||||
</label>
|
||||
|
||||
<label for="admin_display_name">
|
||||
Отображаемое имя
|
||||
<input type="text" id="admin_display_name" name="admin_display_name" value="<?= htmlspecialchars($_POST['admin_display_name'] ?? '') ?>">
|
||||
</label>
|
||||
|
||||
<label for="admin_email">
|
||||
Email
|
||||
<input type="email" id="admin_email" name="admin_email" value="<?= htmlspecialchars($_POST['admin_email'] ?? '') ?>">
|
||||
</label>
|
||||
|
||||
<div class="button-group">
|
||||
<a href="install.php?step=1">Назад</a>
|
||||
<button type="submit">Завершить установку</button>
|
||||
</div>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($step === '1' && !$success): ?>
|
||||
<div style="margin-top: 2rem; padding: 1rem; background: #f8f9fa; border-radius: 5px;">
|
||||
<h4>Перед установкой убедитесь, что:</h4>
|
||||
<ul>
|
||||
<li>Сервер MySQL запущен и доступен</li>
|
||||
<li>У вас есть данные для подключения к БД (хост, пользователь, пароль)</li>
|
||||
<li>Папка <code>config/</code> доступна для записи</li>
|
||||
<li>Папка <code>uploads/</code> доступна для записи</li>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -29,7 +29,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||
$_SESSION['user_id'] = $user['id'];
|
||||
$_SESSION['username'] = $user['username'];
|
||||
$_SESSION['display_name'] = $user['display_name'] ?: $user['username'];
|
||||
|
||||
$_SESSION['avatar'] = $user['avatar'] ?? null;
|
||||
// Обновляем время последнего входа
|
||||
$userModel->updateLastLogin($user['id']);
|
||||
|
||||
|
|
|
|||
|
|
@ -141,5 +141,64 @@ class Book {
|
|||
$stmt = $this->pdo->prepare("UPDATE books SET cover_image = NULL WHERE id = ?");
|
||||
return $stmt->execute([$book_id]);
|
||||
}
|
||||
|
||||
public function updateSeriesInfo($book_id, $series_id, $sort_order) {
|
||||
$stmt = $this->pdo->prepare("UPDATE books SET series_id = ?, sort_order_in_series = ? WHERE id = ?");
|
||||
return $stmt->execute([$series_id, $sort_order, $book_id]);
|
||||
}
|
||||
|
||||
public function removeFromSeries($book_id) {
|
||||
$stmt = $this->pdo->prepare("UPDATE books SET series_id = NULL, sort_order_in_series = NULL WHERE id = ?");
|
||||
return $stmt->execute([$book_id]);
|
||||
}
|
||||
|
||||
public function findBySeries($series_id) {
|
||||
$stmt = $this->pdo->prepare("
|
||||
SELECT b.*
|
||||
FROM books b
|
||||
WHERE b.series_id = ?
|
||||
ORDER BY b.sort_order_in_series, b.created_at
|
||||
");
|
||||
$stmt->execute([$series_id]);
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
public function reorderSeriesBooks($series_id, $new_order) {
|
||||
try {
|
||||
$this->pdo->beginTransaction();
|
||||
|
||||
foreach ($new_order as $order => $book_id) {
|
||||
$stmt = $this->pdo->prepare("UPDATE books SET sort_order_in_series = ? WHERE id = ? AND series_id = ?");
|
||||
$stmt->execute([$order + 1, $book_id, $series_id]);
|
||||
}
|
||||
|
||||
$this->pdo->commit();
|
||||
return true;
|
||||
} catch (Exception $e) {
|
||||
$this->pdo->rollBack();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function getBookStats($book_id, $only_published_chapters = false) {
|
||||
$sql = "
|
||||
SELECT
|
||||
COUNT(c.id) as chapter_count,
|
||||
COALESCE(SUM(c.word_count), 0) as total_words
|
||||
FROM books b
|
||||
LEFT JOIN chapters c ON b.id = c.book_id
|
||||
WHERE b.id = ?
|
||||
";
|
||||
|
||||
if ($only_published_chapters) {
|
||||
$sql .= " AND c.status = 'published'";
|
||||
}
|
||||
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->execute([$book_id]);
|
||||
return $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
}
|
||||
?>
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
<?php
|
||||
// models/Series.php
|
||||
|
||||
class Series {
|
||||
private $pdo;
|
||||
|
||||
public function __construct($pdo) {
|
||||
$this->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);
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
|
@ -86,5 +86,35 @@ class User {
|
|||
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);
|
||||
}
|
||||
}
|
||||
?>
|
||||
125
profile.php
125
profile.php
|
|
@ -7,6 +7,7 @@ $userModel = new User($pdo);
|
|||
$user = $userModel->findById($user_id);
|
||||
|
||||
$message = '';
|
||||
$avatar_error = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
|
||||
|
|
@ -14,9 +15,34 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||
} else {
|
||||
$display_name = trim($_POST['display_name'] ?? '');
|
||||
$email = trim($_POST['email'] ?? '');
|
||||
$bio = trim($_POST['bio'] ?? '');
|
||||
|
||||
$stmt = $pdo->prepare("UPDATE users SET display_name = ?, email = ? WHERE id = ?");
|
||||
if ($stmt->execute([$display_name, $email, $user_id])) {
|
||||
// Обработка загрузки аватарки
|
||||
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 = "Профиль обновлен";
|
||||
// Обновляем данные пользователя
|
||||
|
|
@ -39,8 +65,10 @@ include 'views/header.php';
|
|||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<article>
|
||||
<form method="post">
|
||||
<div class="grid">
|
||||
<article>
|
||||
<h2>Основная информация</h2>
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf_token" value="<?= generate_csrf_token() ?>">
|
||||
|
||||
<div style="margin-bottom: 1rem;">
|
||||
|
|
@ -52,17 +80,33 @@ include 'views/header.php';
|
|||
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<label for="display_name" style="display: block; margin-bottom: 0.5rem; font-weight: bold;">
|
||||
Отображаемое имя
|
||||
Отображаемое имя *
|
||||
</label>
|
||||
<input type="text" id="display_name" name="display_name"
|
||||
value="<?= e($user['display_name'] ?? $user['username']) ?>" style="width: 100%;">
|
||||
value="<?= e($user['display_name'] ?? $user['username']) ?>"
|
||||
style="width: 100%;" required>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
<label for="email" style="display: block; margin-bottom: 0.5rem; font-weight: bold;">
|
||||
Email
|
||||
</label>
|
||||
<input type="email" id="email" name="email" value="<?= e($user['email'] ?? '') ?>" style="width: 100%;">
|
||||
<input type="email" id="email" name="email"
|
||||
value="<?= e($user['email'] ?? '') ?>"
|
||||
style="width: 100%;">
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
<label for="bio" style="display: block; margin-bottom: 0.5rem; font-weight: bold;">
|
||||
О себе (отображается на вашей публичной странице)
|
||||
</label>
|
||||
<textarea id="bio" name="bio"
|
||||
placeholder="Расскажите о себе, своих интересах, стиле письма..."
|
||||
rows="6"
|
||||
style="width: 100%;"><?= e($user['bio'] ?? '') ?></textarea>
|
||||
<small style="color: #666;">
|
||||
Поддерживается Markdown форматирование
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="profile-buttons">
|
||||
|
|
@ -74,11 +118,74 @@ include 'views/header.php';
|
|||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</article>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<h2>Аватарка</h2>
|
||||
|
||||
<div style="text-align: center; margin-bottom: 1.5rem;">
|
||||
<?php if (!empty($user['avatar'])): ?>
|
||||
<img src="<?= AVATARS_URL . e($user['avatar']) ?>"
|
||||
alt="Аватарка"
|
||||
style="max-width: 200px; height: auto; border-radius: 50%; border: 3px solid #007bff;"
|
||||
onerror="this.style.display='none'">
|
||||
<?php else: ?>
|
||||
<div style="width: 200px; height: 200px; border-radius: 50%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; align-items: center; justify-content: center; color: white; font-size: 4rem; margin: 0 auto;">
|
||||
<?= mb_substr(e($user['display_name'] ?? $user['username']), 0, 1) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf_token" value="<?= generate_csrf_token() ?>">
|
||||
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<label for="avatar" style="display: block; margin-bottom: 0.5rem; font-weight: bold;">
|
||||
Загрузить новую аватарку
|
||||
</label>
|
||||
<input type="file" id="avatar" name="avatar"
|
||||
accept="image/jpeg, image/png, image/gif, image/webp"
|
||||
style="height: 2.6rem;">
|
||||
<small style="color: #666;">
|
||||
Разрешены: JPG, PNG, GIF, WebP. Максимальный размер: 2MB.
|
||||
Рекомендуемый размер: 200×200 пикселей.
|
||||
</small>
|
||||
|
||||
<?php if (!empty($avatar_error)): ?>
|
||||
<div style="color: #d32f2f; margin-top: 0.5rem;">
|
||||
❌ <?= e($avatar_error) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<button type="submit" class="contrast" style="flex: 1;">
|
||||
📤 Загрузить аватарку
|
||||
</button>
|
||||
|
||||
<?php if (!empty($user['avatar'])): ?>
|
||||
<button type="submit" name="delete_avatar" value="1" class="secondary" style="flex: 1; background: #ff4444; border-color: #ff4444; color: white;">
|
||||
🗑️ Удалить аватарку
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php if (!empty($user['avatar'])): ?>
|
||||
<div style="margin-top: 1rem; padding: 1rem; background: #f8f9fa; border-radius: 5px;">
|
||||
<p style="margin: 0; font-size: 0.9em; color: #666;">
|
||||
<strong>Примечание:</strong> Аватарка отображается на вашей публичной странице автора
|
||||
</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<article>
|
||||
<h3>Информация об аккаунте</h3>
|
||||
<p><a href="author.php?id=<?= $_SESSION['user_id'] ?>" target="_blank">Моя публичная страница</a></p>
|
||||
<p><a href="author.php?id=<?= $_SESSION['user_id'] ?>" target="_blank" class="adaptive-button secondary">
|
||||
👁️ Посмотреть мою публичную страницу
|
||||
</a></p>
|
||||
<p><strong>Дата регистрации:</strong> <?= date('d.m.Y H:i', strtotime($user['created_at'])) ?></p>
|
||||
<?php if ($user['last_login']): ?>
|
||||
<p><strong>Последний вход:</strong> <?= date('d.m.Y H:i', strtotime($user['last_login'])) ?></p>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
<?php
|
||||
require_once 'config/config.php';
|
||||
require_login();
|
||||
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$seriesModel = new Series($pdo);
|
||||
$series = $seriesModel->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';
|
||||
?>
|
||||
|
||||
<h1>Мои серии книг</h1>
|
||||
|
||||
<?php if (isset($_SESSION['success'])): ?>
|
||||
<div class="alert alert-success">
|
||||
<?= e($_SESSION['success']) ?>
|
||||
<?php unset($_SESSION['success']); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (isset($_SESSION['error'])): ?>
|
||||
<div class="alert alert-error">
|
||||
<?= e($_SESSION['error']) ?>
|
||||
<?php unset($_SESSION['error']); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
|
||||
<h2 style="margin: 0;">Всего серий: <?= count($series) ?></h2>
|
||||
<a href="series_edit.php" class="action-button primary">➕ Новая серия</a>
|
||||
</div>
|
||||
|
||||
<?php if (empty($series)): ?>
|
||||
<article style="text-align: center; padding: 2rem;">
|
||||
<h3>У вас пока нет серий книг</h3>
|
||||
<p>Создайте свою первую серию для организации книг!</p>
|
||||
<a href="series_edit.php" role="button">📚 Создать первую серию</a>
|
||||
</article>
|
||||
<?php else: ?>
|
||||
<div class="grid">
|
||||
<?php foreach ($series as $ser): ?>
|
||||
<article>
|
||||
<header>
|
||||
<h3>
|
||||
<?= e($ser['title']) ?>
|
||||
<div style="display: flex; gap: 3px; float:right;">
|
||||
<a href="series_edit.php?id=<?= $ser['id'] ?>" class="compact-button secondary" title="Редактировать серию">
|
||||
✏️
|
||||
</a>
|
||||
<a href="view_series.php?id=<?= $ser['id'] ?>" class="compact-button secondary" title="Просмотреть серию">
|
||||
👁️
|
||||
</a>
|
||||
<form method="post" action="series_delete.php" style="display: inline;" onsubmit="return confirm('Вы уверены, что хотите удалить серию «<?= e($ser['title']) ?>»? Книги останутся, но будут убраны из серии.');">
|
||||
<input type="hidden" name="series_id" value="<?= $ser['id'] ?>">
|
||||
<input type="hidden" name="csrf_token" value="<?= generate_csrf_token() ?>">
|
||||
<button type="submit" class="compact-button secondary" style="background: #ff4444; border-color: #ff4444; color: white;" title="Удалить серию">
|
||||
🗑️
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</h3>
|
||||
</header>
|
||||
|
||||
<?php if ($ser['description']): ?>
|
||||
<p><?= e(mb_strimwidth($ser['description'], 0, 200, '...')) ?></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<footer>
|
||||
<div>
|
||||
<small>
|
||||
Книг: <?= $ser['book_count'] ?> |
|
||||
Слов: <?= $ser['total_words'] ?>
|
||||
</small>
|
||||
</div>
|
||||
<div style="margin-top: 0.5rem;">
|
||||
<a href="view_series.php?id=<?= $ser['id'] ?>" class="adaptive-button secondary">
|
||||
📖 Смотреть книги
|
||||
</a>
|
||||
</div>
|
||||
</footer>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php include 'views/footer.php'; ?>
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
require_once 'config/config.php';
|
||||
require_login();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
$_SESSION['error'] = "Неверный метод запроса";
|
||||
redirect('series.php');
|
||||
}
|
||||
|
||||
if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
|
||||
$_SESSION['error'] = "Ошибка безопасности";
|
||||
redirect('series.php');
|
||||
}
|
||||
|
||||
$series_id = $_POST['series_id'] ?? null;
|
||||
$user_id = $_SESSION['user_id'];
|
||||
|
||||
if (!$series_id) {
|
||||
$_SESSION['error'] = "Не указана серия для удаления";
|
||||
redirect('series.php');
|
||||
}
|
||||
|
||||
$seriesModel = new Series($pdo);
|
||||
|
||||
if (!$seriesModel->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');
|
||||
?>
|
||||
|
|
@ -0,0 +1,179 @@
|
|||
<?php
|
||||
require_once 'config/config.php';
|
||||
require_login();
|
||||
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$seriesModel = new Series($pdo);
|
||||
|
||||
$series_id = $_GET['id'] ?? null;
|
||||
$series = null;
|
||||
$is_edit = false;
|
||||
|
||||
if ($series_id) {
|
||||
$series = $seriesModel->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';
|
||||
?>
|
||||
|
||||
<h1><?= $is_edit ? "Редактирование серии" : "Создание новой серии" ?></h1>
|
||||
|
||||
<?php if (isset($_SESSION['error'])): ?>
|
||||
<div class="alert alert-error">
|
||||
<?= e($_SESSION['error']) ?>
|
||||
<?php unset($_SESSION['error']); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post">
|
||||
<input type="hidden" name="csrf_token" value="<?= generate_csrf_token() ?>">
|
||||
|
||||
<div style="max-width: 100%; margin-bottom: 1rem;">
|
||||
<label for="title" style="display: block; margin-bottom: 0.5rem; font-weight: bold;">
|
||||
Название серии *
|
||||
</label>
|
||||
<input type="text" id="title" name="title"
|
||||
value="<?= e($series['title'] ?? $_POST['title'] ?? '') ?>"
|
||||
placeholder="Введите название серии"
|
||||
style="width: 100%; margin-bottom: 1.5rem;"
|
||||
required>
|
||||
|
||||
<label for="description" style="display: block; margin-bottom: 0.5rem; font-weight: bold;">
|
||||
Описание серии
|
||||
</label>
|
||||
<textarea id="description" name="description"
|
||||
placeholder="Описание сюжета серии, общая концепция..."
|
||||
rows="6"
|
||||
style="width: 100;"><?= e($series['description'] ?? $_POST['description'] ?? '') ?></textarea>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
|
||||
<button type="submit" class="contrast">
|
||||
<?= $is_edit ? '💾 Сохранить изменения' : '📚 Создать серию' ?>
|
||||
</button>
|
||||
|
||||
<a href="series.php" role="button" class="secondary">
|
||||
❌ Отмена
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php if ($is_edit): ?>
|
||||
<div style="margin-top: 3rem;">
|
||||
<h3>Книги в этой серии</h3>
|
||||
|
||||
<?php
|
||||
$bookModel = new Book($pdo);
|
||||
$books_in_series = $bookModel->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;
|
||||
}
|
||||
?>
|
||||
|
||||
<?php if (empty($books_in_series)): ?>
|
||||
<div style="text-align: center; padding: 2rem; background: #f9f9f9; border-radius: 5px;">
|
||||
<p>В этой серии пока нет книг.</p>
|
||||
<a href="books.php" class="adaptive-button">📚 Добавить книги</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div style="overflow-x: auto;">
|
||||
<table class="compact-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 10%;">Порядок</th>
|
||||
<th style="width: 40%;">Название книги</th>
|
||||
<th style="width: 20%;">Жанр</th>
|
||||
<th style="width: 15%;">Статус</th>
|
||||
<th style="width: 15%;">Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($books_in_series as $book): ?>
|
||||
<tr>
|
||||
<td><?= $book['sort_order_in_series'] ?></td>
|
||||
<td>
|
||||
<strong><?= e($book['title']) ?></strong>
|
||||
<?php if ($book['description']): ?>
|
||||
<br><small style="color: #666;"><?= e(mb_strimwidth($book['description'], 0, 100, '...')) ?></small>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= e($book['genre']) ?></td>
|
||||
<td>
|
||||
<span style="color: <?= $book['published'] ? 'green' : 'orange' ?>">
|
||||
<?= $book['published'] ? '✅ Опубликована' : '📝 Черновик' ?>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<a href="book_edit.php?id=<?= $book['id'] ?>" class="compact-button secondary">
|
||||
Редактировать
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 1rem; padding: 0.5rem; background: #f5f5f5; border-radius: 3px;">
|
||||
<strong>Статистика серии:</strong>
|
||||
Книг: <?= count($books_in_series) ?> |
|
||||
Глав: <?= $total_chapters ?> |
|
||||
Слов: <?= $total_words ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php include 'views/footer.php'; ?>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 82 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 172 KiB |
0
vendor/phpoffice/math/src/Math/Element/AbstractGroupElement.php
vendored
Normal file → Executable file
0
vendor/phpoffice/math/src/Math/Element/AbstractGroupElement.php
vendored
Normal file → Executable file
0
vendor/phpoffice/math/src/Math/Exception/InvalidInputException.php
vendored
Normal file → Executable file
0
vendor/phpoffice/math/src/Math/Exception/InvalidInputException.php
vendored
Normal file → Executable file
0
vendor/phpoffice/math/src/Math/Exception/NotImplementedException.php
vendored
Normal file → Executable file
0
vendor/phpoffice/math/src/Math/Exception/NotImplementedException.php
vendored
Normal file → Executable file
0
vendor/phpoffice/math/src/Math/Exception/SecurityException.php
vendored
Normal file → Executable file
0
vendor/phpoffice/math/src/Math/Exception/SecurityException.php
vendored
Normal file → Executable file
0
vendor/phpoffice/math/src/Math/Reader/Security/XmlScanner.php
vendored
Normal file → Executable file
0
vendor/phpoffice/math/src/Math/Reader/Security/XmlScanner.php
vendored
Normal file → Executable file
0
vendor/phpoffice/math/tests/Math/Element/AbstractGroupElementTest.php
vendored
Normal file → Executable file
0
vendor/phpoffice/math/tests/Math/Element/AbstractGroupElementTest.php
vendored
Normal file → Executable file
0
vendor/phpoffice/math/tests/Math/Element/IdentifierTest.php
vendored
Normal file → Executable file
0
vendor/phpoffice/math/tests/Math/Element/IdentifierTest.php
vendored
Normal file → Executable file
0
vendor/phpoffice/math/tests/Math/Element/SuperscriptTest.php
vendored
Normal file → Executable file
0
vendor/phpoffice/math/tests/Math/Element/SuperscriptTest.php
vendored
Normal file → Executable file
0
vendor/phpoffice/math/tests/Math/Reader/OfficeMathMLTest.php
vendored
Normal file → Executable file
0
vendor/phpoffice/math/tests/Math/Reader/OfficeMathMLTest.php
vendored
Normal file → Executable file
0
vendor/phpoffice/math/tests/Math/Writer/OfficeMathMLTest.php
vendored
Normal file → Executable file
0
vendor/phpoffice/math/tests/Math/Writer/OfficeMathMLTest.php
vendored
Normal file → Executable file
0
vendor/phpoffice/math/tests/resources/schema/mathml3/mathml3-common.xsd
vendored
Normal file → Executable file
0
vendor/phpoffice/math/tests/resources/schema/mathml3/mathml3-common.xsd
vendored
Normal file → Executable file
0
vendor/phpoffice/math/tests/resources/schema/mathml3/mathml3-content.xsd
vendored
Normal file → Executable file
0
vendor/phpoffice/math/tests/resources/schema/mathml3/mathml3-content.xsd
vendored
Normal file → Executable file
0
vendor/phpoffice/math/tests/resources/schema/mathml3/mathml3-presentation.xsd
vendored
Normal file → Executable file
0
vendor/phpoffice/math/tests/resources/schema/mathml3/mathml3-presentation.xsd
vendored
Normal file → Executable file
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue