feat: Add complete reviews system with star ratings

 New Features:
- Reviews system with 1-5 star ratings
- User can add, edit, and delete their own reviews
- One review per product per user (DB constraint)
- Automatic average rating calculation
- Review count tracking
- Interactive star selection UI
- AJAX-powered review submission
- Responsive design for all devices

🗄️ Database:
- New 'reviews' table with full structure
- Added 'rating' and 'review_count' fields to products
- PostgreSQL triggers for automatic rating updates
- Database functions for rating calculations
- Indexes for performance optimization

📦 Backend (PHP):
- Review model with 15+ methods
- ReviewController with 5 actions
- Updated Product model to include ratings
- Updated ProductController to load reviews
- 5 new API endpoints

🎨 Frontend:
- Reviews list component (_reviews_list.php)
- Review form component (_review_form.php)
- Reviews sechow page
- Star ratings in catalog view
- Interactive JavaScript (200+ lines)
- Adaptive styles (400+ lines)

🔒 Security:
- Server-side authorization checks
- XSS protection (htmlspecialchars)
- SQL injection protection (PDO prepared)
- Input validation (client + server)
- Access control for review editing

📝 Modified Files:
- app/Models/Product.php - added rating fields to queries
- app/Controllers/ProductController.php - loads reviews
- app/Views/products/show.php - reviews section
- app/Views/products/catalog.php - star ratings
- config/routes.php - review endpoints
- public/style_for_cite.less - rating styles

🆕 New Files:
- app/Models/Review.php
- app/Controllers/ReviewController.php
- app/Views/products/_reviews_list.php
- app/Views/products/_review_form.php
This commit is contained in:
kirill.khorkov
2026-01-06 17:04:09 +03:00
parent 547c561ed0
commit a4092adf2e
17 changed files with 1646 additions and 59 deletions

View File

@@ -38,7 +38,17 @@
<body>
<div id="notification" class="notification"></div>
<?= \App\Core\View::partial('header', ['user' => $user ?? null, 'isLoggedIn' => $isLoggedIn ?? \App\Core\View::isAuthenticated(), 'isAdmin' => $isAdmin ?? \App\Core\View::isAdmin()]) ?>
<?php
// Загружаем категории для header
$categoryModel = new \App\Models\Category();
$headerCategories = $categoryModel->getActive();
?>
<?= \App\Core\View::partial('header', [
'user' => $user ?? null,
'isLoggedIn' => $isLoggedIn ?? \App\Core\View::isAuthenticated(),
'isAdmin' => $isAdmin ?? \App\Core\View::isAdmin(),
'categories' => $headerCategories ?? []
]) ?>
<main>
<?= $content ?>
@@ -55,6 +65,68 @@
setTimeout(function() { notification.removeClass('show'); }, 3000);
}
// Выпадающий список категорий
(function() {
function initCatalogDropdown() {
var catalogDropdown = document.getElementById('catalogDropdown');
var catalogMenu = document.getElementById('catalogMenu');
if (!catalogDropdown || !catalogMenu) {
return;
}
// Обработчик клика на выпадающий список
catalogDropdown.addEventListener('click', function(e) {
e.stopPropagation();
var isActive = this.classList.contains('active');
if (isActive) {
this.classList.remove('active');
catalogMenu.style.display = 'none';
} else {
this.classList.add('active');
catalogMenu.style.display = 'block';
}
});
// Закрытие при клике вне меню
document.addEventListener('click', function(e) {
if (catalogDropdown && !catalogDropdown.contains(e.target)) {
catalogDropdown.classList.remove('active');
if (catalogMenu) {
catalogMenu.style.display = 'none';
}
}
});
// Закрытие при клике на ссылку в меню (через делегирование)
if (catalogMenu) {
catalogMenu.addEventListener('click', function(e) {
if (e.target.tagName === 'A') {
catalogDropdown.classList.remove('active');
catalogMenu.style.display = 'none';
}
});
}
}
// Инициализация при загрузке DOM
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initCatalogDropdown);
} else {
initCatalogDropdown();
}
// Резервная инициализация при полной загрузке страницы
window.addEventListener('load', function() {
var catalogDropdown = document.getElementById('catalogDropdown');
if (catalogDropdown && !catalogDropdown.hasAttribute('data-initialized')) {
catalogDropdown.setAttribute('data-initialized', 'true');
initCatalogDropdown();
}
});
})();
$(document).ready(function() {
$.get('/cart/count', function(response) {
if (response.success) {