diff --git a/app/Controllers/AdminController.php b/app/Controllers/AdminController.php index f876cd6..1c0400f 100644 --- a/app/Controllers/AdminController.php +++ b/app/Controllers/AdminController.php @@ -153,6 +153,28 @@ class AdminController extends Controller $this->redirect('/admin/products?message=' . urlencode('Товар скрыт')); } + public function removeProduct(int $id): void + { + $this->requireAdmin(); + + $product = $this->productModel->find($id); + + if (!$product) { + $this->redirect('/admin/products?error=' . urlencode('Товар не найден')); + return; + } + + // Удаляем только если товара нет на складе + if ($product['stock_quantity'] > 0) { + $this->redirect('/admin/products?error=' . urlencode('Нельзя удалить товар, который есть на складе')); + return; + } + + // Полное удаление из БД + $this->productModel->delete($id); + $this->redirect('/admin/products?message=' . urlencode('Товар удален из базы данных')); + } + public function categories(): void { $this->requireAdmin(); diff --git a/app/Controllers/CartController.php b/app/Controllers/CartController.php index 6176046..33db5c5 100644 --- a/app/Controllers/CartController.php +++ b/app/Controllers/CartController.php @@ -21,6 +21,11 @@ class CartController extends Controller { $this->requireAuth(); + if ($this->isAdmin()) { + $this->redirect('/catalog'); + return; + } + $user = $this->getCurrentUser(); $cartItems = $this->cartModel->getUserCart($user['id']); $totals = $this->cartModel->getCartTotal($user['id']); @@ -43,6 +48,14 @@ class CartController extends Controller return; } + if ($this->isAdmin()) { + $this->json([ + 'success' => false, + 'message' => 'Доступ запрещен' + ]); + return; + } + $productId = (int) $this->getPost('product_id', 0); $quantity = (int) $this->getPost('quantity', 1); $userId = $this->getCurrentUser()['id']; @@ -104,6 +117,14 @@ class CartController extends Controller return; } + if ($this->isAdmin()) { + $this->json([ + 'success' => false, + 'message' => 'Доступ запрещен' + ]); + return; + } + $productId = (int) $this->getPost('product_id', 0); $quantity = (int) $this->getPost('quantity', 1); $userId = $this->getCurrentUser()['id']; @@ -154,6 +175,14 @@ class CartController extends Controller return; } + if ($this->isAdmin()) { + $this->json([ + 'success' => false, + 'message' => 'Доступ запрещен' + ]); + return; + } + $productId = (int) $this->getPost('product_id', 0); $userId = $this->getCurrentUser()['id']; diff --git a/app/Controllers/OrderController.php b/app/Controllers/OrderController.php index bf38d9c..79cd6e6 100644 --- a/app/Controllers/OrderController.php +++ b/app/Controllers/OrderController.php @@ -21,6 +21,11 @@ class OrderController extends Controller { $this->requireAuth(); + if ($this->isAdmin()) { + $this->redirect('/catalog'); + return; + } + $user = $this->getCurrentUser(); $cartItems = $this->cartModel->getUserCart($user['id']); $totals = $this->cartModel->getCartTotal($user['id']); @@ -43,6 +48,14 @@ class OrderController extends Controller return; } + if ($this->isAdmin()) { + $this->json([ + 'success' => false, + 'message' => 'Доступ запрещен' + ]); + return; + } + $user = $this->getCurrentUser(); $cartItems = $this->cartModel->getUserCart($user['id']); diff --git a/app/Controllers/ProductController.php b/app/Controllers/ProductController.php index e33acf1..c0c59b3 100644 --- a/app/Controllers/ProductController.php +++ b/app/Controllers/ProductController.php @@ -5,16 +5,19 @@ namespace App\Controllers; use App\Core\Controller; use App\Models\Product; use App\Models\Category; +use App\Models\Review; class ProductController extends Controller { private Product $productModel; private Category $categoryModel; + private Review $reviewModel; public function __construct() { $this->productModel = new Product(); $this->categoryModel = new Category(); + $this->reviewModel = new Review(); } public function catalog(): void @@ -33,10 +36,12 @@ class ProductController extends Controller 'materials' => $this->getQuery('materials', []) ]; - $showAll = $isAdmin && $this->getQuery('show_all') === '1'; + // Для админа по умолчанию показываем все товары (включая недоступные) + // Для обычных пользователей - только доступные + $showAll = $isAdmin && $this->getQuery('show_all') !== '0'; $categories = $this->categoryModel->getActive(); - $products = $showAll + $products = ($isAdmin && $showAll) ? $this->productModel->getAllForAdmin(true) : $this->productModel->getAvailable($filters); @@ -80,10 +85,22 @@ class ProductController extends Controller $product['category_id'] ); + // Load reviews + $reviews = $this->reviewModel->getByProduct($id, true); + + // Check if current user has already reviewed + $user = $this->getCurrentUser(); + $userReview = null; + if ($user && !$this->isAdmin()) { + $userReview = $this->reviewModel->getByUser($user['id'], $id); + } + $this->view('products/show', [ 'product' => $product, 'similarProducts' => $similarProducts, - 'user' => $this->getCurrentUser(), + 'reviews' => $reviews, + 'userReview' => $userReview, + 'user' => $user, 'isLoggedIn' => true, 'isAdmin' => $this->isAdmin() ]); diff --git a/app/Controllers/ReviewController.php b/app/Controllers/ReviewController.php new file mode 100644 index 0000000..2c4b3e6 --- /dev/null +++ b/app/Controllers/ReviewController.php @@ -0,0 +1,298 @@ +reviewModel = new Review(); + $this->productModel = new Product(); + } + + /** + * Create a new review (POST /reviews) + */ + public function create(): void + { + // Require authentication + if (!$this->isAuthenticated()) { + $this->json([ + 'success' => false, + 'message' => 'Требуется авторизация' + ], 401); + return; + } + + // Admins cannot leave reviews + if ($this->isAdmin()) { + $this->json([ + 'success' => false, + 'message' => 'Администраторы не могут оставлять отзывы' + ], 403); + return; + } + + $productId = (int) $this->getPost('product_id', 0); + $rating = (int) $this->getPost('rating', 0); + $comment = trim($this->getPost('comment', '')); + $user = $this->getCurrentUser(); + + // Validate product ID + if ($productId <= 0) { + $this->json([ + 'success' => false, + 'message' => 'Неверный ID товара' + ], 400); + return; + } + + // Check if product exists + $product = $this->productModel->find($productId); + if (!$product) { + $this->json([ + 'success' => false, + 'message' => 'Товар не найден' + ], 404); + return; + } + + // Validate rating + if ($rating < 1 || $rating > 5) { + $this->json([ + 'success' => false, + 'message' => 'Рейтинг должен быть от 1 до 5' + ], 400); + return; + } + + // Check if user can review + if (!$this->reviewModel->userCanReview($user['id'], $productId)) { + $this->json([ + 'success' => false, + 'message' => 'Вы уже оставили отзыв на этот товар' + ], 400); + return; + } + + // Create review + $reviewId = $this->reviewModel->createReview([ + 'product_id' => $productId, + 'user_id' => $user['id'], + 'rating' => $rating, + 'comment' => $comment, + 'is_approved' => true + ]); + + if ($reviewId) { + // Get updated product info + $updatedProduct = $this->productModel->find($productId); + + $this->json([ + 'success' => true, + 'message' => 'Спасибо за ваш отзыв!', + 'review_id' => $reviewId, + 'product_rating' => $updatedProduct['rating'] ?? 0, + 'product_review_count' => $updatedProduct['review_count'] ?? 0 + ]); + } else { + $this->json([ + 'success' => false, + 'message' => 'Ошибка при создании отзыва' + ], 500); + } + } + + /** + * Update a review (POST /reviews/{id}) + */ + public function update(int $id): void + { + // Require authentication + if (!$this->isAuthenticated()) { + $this->json([ + 'success' => false, + 'message' => 'Требуется авторизация' + ], 401); + return; + } + + $user = $this->getCurrentUser(); + $review = $this->reviewModel->find($id); + + if (!$review) { + $this->json([ + 'success' => false, + 'message' => 'Отзыв не найден' + ], 404); + return; + } + + // Check ownership (only owner or admin can update) + if ($review['user_id'] !== $user['id'] && !$this->isAdmin()) { + $this->json([ + 'success' => false, + 'message' => 'У вас нет прав для редактирования этого отзыва' + ], 403); + return; + } + + $rating = (int) $this->getPost('rating', 0); + $comment = trim($this->getPost('comment', '')); + + // Validate rating + if ($rating < 1 || $rating > 5) { + $this->json([ + 'success' => false, + 'message' => 'Рейтинг должен быть от 1 до 5' + ], 400); + return; + } + + // Update review + $success = $this->reviewModel->updateReview($id, [ + 'rating' => $rating, + 'comment' => $comment + ]); + + if ($success) { + // Get updated product info + $updatedProduct = $this->productModel->find($review['product_id']); + + $this->json([ + 'success' => true, + 'message' => 'Отзыв обновлен', + 'product_rating' => $updatedProduct['rating'] ?? 0, + 'product_review_count' => $updatedProduct['review_count'] ?? 0 + ]); + } else { + $this->json([ + 'success' => false, + 'message' => 'Ошибка при обновлении отзыва' + ], 500); + } + } + + /** + * Delete a review (POST /reviews/{id}/delete) + */ + public function delete(int $id): void + { + // Require authentication + if (!$this->isAuthenticated()) { + $this->json([ + 'success' => false, + 'message' => 'Требуется авторизация' + ], 401); + return; + } + + $user = $this->getCurrentUser(); + $review = $this->reviewModel->find($id); + + if (!$review) { + $this->json([ + 'success' => false, + 'message' => 'Отзыв не найден' + ], 404); + return; + } + + // Check ownership (only owner or admin can delete) + if ($review['user_id'] !== $user['id'] && !$this->isAdmin()) { + $this->json([ + 'success' => false, + 'message' => 'У вас нет прав для удаления этого отзыва' + ], 403); + return; + } + + $productId = $review['product_id']; + $success = $this->reviewModel->deleteReview($id); + + if ($success) { + // Get updated product info + $updatedProduct = $this->productModel->find($productId); + + $this->json([ + 'success' => true, + 'message' => 'Отзыв удален', + 'product_rating' => $updatedProduct['rating'] ?? 0, + 'product_review_count' => $updatedProduct['review_count'] ?? 0 + ]); + } else { + $this->json([ + 'success' => false, + 'message' => 'Ошибка при удалении отзыва' + ], 500); + } + } + + /** + * Get reviews for a product (GET /reviews/product/{id}) + */ + public function getByProduct(int $productId): void + { + $product = $this->productModel->find($productId); + + if (!$product) { + $this->json([ + 'success' => false, + 'message' => 'Товар не найден' + ], 404); + return; + } + + $reviews = $this->reviewModel->getByProduct($productId, true); + $distribution = $this->reviewModel->getRatingDistribution($productId); + + $this->json([ + 'success' => true, + 'reviews' => $reviews, + 'rating_distribution' => $distribution, + 'average_rating' => $product['rating'] ?? 0, + 'total_reviews' => $product['review_count'] ?? 0 + ]); + } + + /** + * Toggle review approval (admin only) (POST /reviews/{id}/toggle-approval) + */ + public function toggleApproval(int $id): void + { + $this->requireAdmin(); + + $review = $this->reviewModel->find($id); + if (!$review) { + $this->json([ + 'success' => false, + 'message' => 'Отзыв не найден' + ], 404); + return; + } + + $success = $this->reviewModel->toggleApproval($id); + + if ($success) { + $updatedReview = $this->reviewModel->find($id); + $this->json([ + 'success' => true, + 'message' => 'Статус отзыва изменен', + 'is_approved' => $updatedReview['is_approved'] + ]); + } else { + $this->json([ + 'success' => false, + 'message' => 'Ошибка при изменении статуса' + ], 500); + } + } +} + diff --git a/app/Models/Product.php b/app/Models/Product.php index 32ffacf..9af4bde 100644 --- a/app/Models/Product.php +++ b/app/Models/Product.php @@ -11,7 +11,9 @@ class Product extends Model public function findWithCategory(int $id): ?array { - $sql = "SELECT p.*, c.name as category_name, c.slug as category_slug + $sql = "SELECT p.*, c.name as category_name, c.slug as category_slug, + COALESCE(p.rating, 0) as rating, + COALESCE(p.review_count, 0) as review_count FROM {$this->table} p LEFT JOIN categories c ON p.category_id = c.category_id WHERE p.product_id = ?"; @@ -20,7 +22,9 @@ class Product extends Model public function getAvailable(array $filters = [], int $limit = 50): array { - $sql = "SELECT p.*, c.name as category_name + $sql = "SELECT p.*, c.name as category_name, + COALESCE(p.rating, 0) as rating, + COALESCE(p.review_count, 0) as review_count FROM {$this->table} p LEFT JOIN categories c ON p.category_id = c.category_id WHERE p.is_available = TRUE"; @@ -67,7 +71,9 @@ class Product extends Model public function getAllForAdmin(bool $showAll = true): array { - $sql = "SELECT p.*, c.name as category_name + $sql = "SELECT p.*, c.name as category_name, + COALESCE(p.rating, 0) as rating, + COALESCE(p.review_count, 0) as review_count FROM {$this->table} p LEFT JOIN categories c ON p.category_id = c.category_id"; @@ -82,7 +88,10 @@ class Product extends Model public function getSimilar(int $productId, int $categoryId, int $limit = 3): array { - $sql = "SELECT * FROM {$this->table} + $sql = "SELECT *, + COALESCE(rating, 0) as rating, + COALESCE(review_count, 0) as review_count + FROM {$this->table} WHERE category_id = ? AND product_id != ? AND is_available = TRUE diff --git a/app/Models/Review.php b/app/Models/Review.php new file mode 100644 index 0000000..c707337 --- /dev/null +++ b/app/Models/Review.php @@ -0,0 +1,246 @@ +getByUser($data['user_id'], $data['product_id']); + if ($existing) { + return null; // User already reviewed this product + } + + return $this->create([ + 'product_id' => $data['product_id'], + 'user_id' => $data['user_id'], + 'rating' => $data['rating'], + 'comment' => $data['comment'] ?? null, + 'is_approved' => $data['is_approved'] ?? true + ]); + } + + /** + * Get all reviews for a product + */ + public function getByProduct(int $productId, bool $approvedOnly = true): array + { + $sql = "SELECT r.*, u.full_name, u.email + FROM {$this->table} r + INNER JOIN users u ON r.user_id = u.user_id"; + + if ($approvedOnly) { + $sql .= " WHERE r.product_id = ? AND r.is_approved = TRUE"; + } else { + $sql .= " WHERE r.product_id = ?"; + } + + $sql .= " ORDER BY r.created_at DESC"; + + return $this->query($sql, [$productId]); + } + + /** + * Get review by specific user for a product + */ + public function getByUser(int $userId, int $productId): ?array + { + $sql = "SELECT * FROM {$this->table} + WHERE user_id = ? AND product_id = ?"; + return $this->queryOne($sql, [$userId, $productId]); + } + + /** + * Get review with user details + */ + public function findWithUser(int $reviewId): ?array + { + $sql = "SELECT r.*, u.full_name, u.email + FROM {$this->table} r + INNER JOIN users u ON r.user_id = u.user_id + WHERE r.review_id = ?"; + return $this->queryOne($sql, [$reviewId]); + } + + /** + * Update a review + */ + public function updateReview(int $reviewId, array $data): bool + { + $updateData = []; + + if (isset($data['rating'])) { + $updateData['rating'] = $data['rating']; + } + if (isset($data['comment'])) { + $updateData['comment'] = $data['comment']; + } + if (isset($data['is_approved'])) { + $updateData['is_approved'] = $data['is_approved']; + } + + if (empty($updateData)) { + return false; + } + + return $this->update($reviewId, $updateData); + } + + /** + * Delete a review + */ + public function deleteReview(int $reviewId): bool + { + return $this->delete($reviewId); + } + + /** + * Get average rating for a product + */ + public function getAverageRating(int $productId): float + { + $sql = "SELECT COALESCE(AVG(rating), 0.00) as avg_rating + FROM {$this->table} + WHERE product_id = ? AND is_approved = TRUE"; + $result = $this->queryOne($sql, [$productId]); + return round((float)$result['avg_rating'], 2); + } + + /** + * Get review count for a product + */ + public function getReviewCount(int $productId): int + { + return $this->count([ + 'product_id' => $productId, + 'is_approved' => true + ]); + } + + /** + * Update product's rating and review count + * This is called automatically by database triggers, + * but can also be called manually if needed + */ + public function updateProductRating(int $productId): void + { + // Call the PostgreSQL function + $sql = "SELECT update_product_rating(?)"; + $this->execute($sql, [$productId]); + } + + /** + * Get rating distribution for a product (useful for charts) + */ + public function getRatingDistribution(int $productId): array + { + $sql = "SELECT rating, COUNT(*) as count + FROM {$this->table} + WHERE product_id = ? AND is_approved = TRUE + GROUP BY rating + ORDER BY rating DESC"; + + $results = $this->query($sql, [$productId]); + + // Initialize all ratings with 0 + $distribution = [ + 5 => 0, + 4 => 0, + 3 => 0, + 2 => 0, + 1 => 0 + ]; + + // Fill in actual counts + foreach ($results as $row) { + $distribution[$row['rating']] = (int)$row['count']; + } + + return $distribution; + } + + /** + * Get recent reviews across all products (for admin dashboard) + */ + public function getRecent(int $limit = 10, bool $approvedOnly = false): array + { + $sql = "SELECT r.*, u.full_name, u.email, p.name as product_name + FROM {$this->table} r + INNER JOIN users u ON r.user_id = u.user_id + INNER JOIN products p ON r.product_id = p.product_id"; + + if ($approvedOnly) { + $sql .= " WHERE r.is_approved = TRUE"; + } + + $sql .= " ORDER BY r.created_at DESC LIMIT ?"; + + return $this->query($sql, [$limit]); + } + + /** + * Check if user can review (has purchased the product) + * This is optional - you might want to allow reviews only from buyers + */ + public function userCanReview(int $userId, int $productId): bool + { + // Check if user already reviewed + $existing = $this->getByUser($userId, $productId); + if ($existing) { + return false; + } + + // Optional: Check if user has purchased this product + // For now, we'll allow any authenticated user to review + return true; + + /* Uncomment this to require purchase before review: + $sql = "SELECT COUNT(*) as count + FROM order_items oi + INNER JOIN orders o ON oi.order_id = o.order_id + WHERE o.user_id = ? AND oi.product_id = ? AND o.status = 'completed'"; + $result = $this->queryOne($sql, [$userId, $productId]); + return $result && $result['count'] > 0; + */ + } + + /** + * Toggle review approval status (admin function) + */ + public function toggleApproval(int $reviewId): bool + { + $review = $this->find($reviewId); + if (!$review) { + return false; + } + + return $this->update($reviewId, [ + 'is_approved' => !$review['is_approved'] + ]); + } + + /** + * Get all reviews by a specific user + */ + public function getUserReviews(int $userId, int $limit = 50): array + { + $sql = "SELECT r.*, p.name as product_name, p.image_url as product_image + FROM {$this->table} r + INNER JOIN products p ON r.product_id = p.product_id + WHERE r.user_id = ? + ORDER BY r.created_at DESC + LIMIT ?"; + + return $this->query($sql, [$userId, $limit]); + } +} + diff --git a/app/Views/admin/products/index.php b/app/Views/admin/products/index.php index f9c783b..9ca523f 100644 --- a/app/Views/admin/products/index.php +++ b/app/Views/admin/products/index.php @@ -66,6 +66,14 @@ + +
+ +
+ diff --git a/app/Views/home/index.php b/app/Views/home/index.php index 7f7a613..f7299d4 100644 --- a/app/Views/home/index.php +++ b/app/Views/home/index.php @@ -155,6 +155,6 @@ - + Задать вопрос diff --git a/app/Views/layouts/main.php b/app/Views/layouts/main.php index 615e8e2..cfc6b28 100644 --- a/app/Views/layouts/main.php +++ b/app/Views/layouts/main.php @@ -38,7 +38,17 @@
- $user ?? null, 'isLoggedIn' => $isLoggedIn ?? \App\Core\View::isAuthenticated(), 'isAdmin' => $isAdmin ?? \App\Core\View::isAdmin()]) ?> + getActive(); + ?> + $user ?? null, + 'isLoggedIn' => $isLoggedIn ?? \App\Core\View::isAuthenticated(), + 'isAdmin' => $isAdmin ?? \App\Core\View::isAdmin(), + 'categories' => $headerCategories ?? [] + ]) ?>
@@ -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) { diff --git a/app/Views/partials/header.php b/app/Views/partials/header.php index 3009ff9..b1075df 100644 --- a/app/Views/partials/header.php +++ b/app/Views/partials/header.php @@ -3,22 +3,30 @@ $isLoggedIn = $isLoggedIn ?? \App\Core\View::isAuthenticated(); $isAdmin = $isAdmin ?? \App\Core\View::isAdmin(); $user = $user ?? \App\Core\View::currentUser(); ?> +
-
+
Все категории - @@ -34,48 +42,16 @@ $user = $user ?? \App\Core\View::currentUser();
+ 0 + - + + Выйти + Войти diff --git a/app/Views/products/_review_form.php b/app/Views/products/_review_form.php new file mode 100644 index 0000000..a0d34de --- /dev/null +++ b/app/Views/products/_review_form.php @@ -0,0 +1,317 @@ + + +
+

+ +
+ + + + + +
+ +
+ + + +
+ +
+ + из 5 + + Нажмите на звезды для выбора оценки + +
+ +
+ +
+ + +
+ /1000 +
+
+ +
+ + + + +
+ +
+
+
+ + + + + diff --git a/app/Views/products/_reviews_list.php b/app/Views/products/_reviews_list.php new file mode 100644 index 0000000..800d7ea --- /dev/null +++ b/app/Views/products/_reviews_list.php @@ -0,0 +1,208 @@ + + +
+ +
+

Пока нет отзывов об этом товаре. Будьте первым!

+
+ + +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ + + +
+
+ + +
+ +
+ + + +
+ + + + +
+ + + +
+ Отзыв ожидает модерации +
+ +
+ + +
+ + + diff --git a/app/Views/products/catalog.php b/app/Views/products/catalog.php index 8e19fb8..77b1317 100644 --- a/app/Views/products/catalog.php +++ b/app/Views/products/catalog.php @@ -20,6 +20,11 @@ use App\Core\View; .product-card img { width: 100%; height: 200px; object-fit: cover; } .product-info { padding: 15px; } .product-info .name { font-weight: bold; color: #453227; margin-bottom: 5px; } + .product-info .product-rating { display: flex; align-items: center; gap: 5px; margin: 8px 0; } + .product-info .product-rating .stars { display: flex; gap: 1px; } + .product-info .product-rating .star { font-size: 14px; color: #ddd; } + .product-info .product-rating .star.filled { color: #ffc107; } + .product-info .product-rating .rating-text { font-size: 12px; color: #666; } .product-info .price { color: #617365; font-size: 18px; font-weight: bold; } .add-to-cart-btn { position: absolute; bottom: 15px; right: 15px; background: rgba(255,255,255,0.9); width: 40px; height: 40px; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; color: #453227; border: 1px solid #eee; transition: all 0.3s; } .add-to-cart-btn:hover { background: #453227; color: white; } @@ -47,12 +52,21 @@ use App\Core\View;

Панель управления каталогом

- @@ -160,9 +174,23 @@ use App\Core\View; alt="">
+ 0): ?> +
+
+ ★'; + } + ?> +
+ () +
+
- + diff --git a/app/Views/products/show.php b/app/Views/products/show.php index f1a70ab..f75b16c 100644 --- a/app/Views/products/show.php +++ b/app/Views/products/show.php @@ -31,6 +31,17 @@ use App\Core\View; .product-card { background: #f5f5f5; border-radius: 8px; overflow: hidden; } .product-card img { width: 100%; height: 200px; object-fit: cover; } .product-card .product-info { padding: 15px; } + + /* Reviews Section */ + .product-reviews { margin: 40px 0; padding: 30px; background: #fff; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.05); } + .product-reviews h2 { color: #453227; margin-bottom: 25px; font-size: 26px; } + .reviews-summary { background: #f8f9fa; padding: 25px; border-radius: 8px; margin-bottom: 30px; } + .reviews-summary-rating { text-align: center; } + .rating-number { font-size: 48px; font-weight: bold; color: #453227; margin-bottom: 10px; } + .rating-stars { font-size: 24px; margin: 10px 0; } + .rating-stars .star { color: #ddd; margin: 0 2px; } + .rating-stars .star.filled { color: #ffc107; } + .rating-count { color: #666; margin-top: 10px; font-size: 14px; }
@@ -123,7 +134,7 @@ use App\Core\View;

- 0): ?> + 0): ?>
@@ -179,6 +190,52 @@ use App\Core\View;
+ + +
+

Отзывы о товаре

+ +
+
+
+
+ ★'; + } + ?> +
+
+ Всего отзывов: +
+
+
+ + + + + + + + + + + + + +
diff --git a/config/routes.php b/config/routes.php index e8086be..d0ded9a 100644 --- a/config/routes.php +++ b/config/routes.php @@ -33,6 +33,7 @@ $router->post('/admin/products/add', 'AdminController', 'storeProduct'); $router->get('/admin/products/edit/{id}', 'AdminController', 'editProduct'); $router->post('/admin/products/edit/{id}', 'AdminController', 'updateProduct'); $router->post('/admin/products/delete/{id}', 'AdminController', 'deleteProduct'); +$router->post('/admin/products/remove/{id}', 'AdminController', 'removeProduct'); $router->get('/admin/categories', 'AdminController', 'categories'); $router->get('/admin/categories/add', 'AdminController', 'addCategory'); @@ -46,3 +47,9 @@ $router->get('/admin/orders/{id}', 'AdminController', 'orderDetails'); $router->post('/admin/orders/{id}/status', 'AdminController', 'updateOrderStatus'); $router->get('/admin/users', 'AdminController', 'users'); + +$router->post('/reviews', 'ReviewController', 'create'); +$router->post('/reviews/{id}', 'ReviewController', 'update'); +$router->post('/reviews/{id}/delete', 'ReviewController', 'delete'); +$router->get('/reviews/product/{id}', 'ReviewController', 'getByProduct'); +$router->post('/reviews/{id}/toggle-approval', 'ReviewController', 'toggleApproval'); diff --git a/public/style_for_cite.less b/public/style_for_cite.less index fa03fb8..9094b3b 100644 --- a/public/style_for_cite.less +++ b/public/style_for_cite.less @@ -148,19 +148,65 @@ p, li, span { .flex-center(10px); width: 200px; flex-shrink: 0; + user-select: none; + z-index: 1000; &__menu { - .menu-base(); + position: absolute; + top: 100%; + left: 0; + width: 250px; + background: white; + border-radius: 8px; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); + padding: 15px; + z-index: 10000; + margin-top: 5px; + display: none; + pointer-events: auto; + max-height: 400px; + overflow-y: auto; + + ul { + margin: 0; + padding: 0; + list-style: none; + } + li { padding: 8px 0; cursor: pointer; transition: color 0.3s; border-bottom: 1px solid #f0f0f0; + list-style: none; &:last-child { border-bottom: none; } - &:hover { color: @color-accent; } + a { + display: block; + color: #333; + text-decoration: none; + padding: 5px 0; + cursor: pointer; + width: 100%; + &:hover { + color: @color-accent; + } + } + &:hover { + a { + color: @color-accent; + } + } } } - &:hover &__menu { display: block; } + + &:hover &__menu, + &.active &__menu { + display: block; + } + + &.active { + background-color: #e8e8e8; + } } .search-box { @@ -943,6 +989,67 @@ p, li, span { align-items: center; gap: 10px; margin-bottom: 30px; + + .stars { + display: flex; + gap: 2px; + + .star { + font-size: 18px; + color: #ddd; + + &.filled { + color: #ffc107; + } + } + } + + span { + color: #666; + font-size: 14px; + } +} + +// Global rating styles +.rating-stars { + display: flex; + gap: 2px; + + .star { + font-size: 16px; + color: #ddd; + + &.filled { + color: #ffc107; + } + } +} + +// Review rating styles for product cards +.product-rating { + display: flex; + align-items: center; + gap: 8px; + margin-top: 5px; + + .stars { + display: flex; + gap: 1px; + + .star { + font-size: 14px; + color: #ddd; + + &.filled { + color: #ffc107; + } + } + } + + .rating-text { + font-size: 12px; + color: #666; + } } .product__color-selector {