[MVC] Полная миграция на MVC архитектуру

- Создано ядро MVC: App, Router, Controller, Model, View, Database
- Созданы модели: User, Product, Category, Cart, Order
- Созданы контроллеры: Home, Auth, Product, Cart, Order, Page, Admin
- Созданы layouts и partials для представлений
- Добавлены все views для страниц
- Настроена маршрутизация с чистыми URL
- Обновлена конфигурация Docker и Apache для mod_rewrite
- Добавлена единая точка входа public/index.php
This commit is contained in:
kirill.khorkov
2026-01-03 11:48:14 +03:00
parent 3f257120fa
commit d2c15ec37f
53 changed files with 8650 additions and 30 deletions

View File

@@ -0,0 +1,195 @@
<?php
$title = 'Каталог';
use App\Core\View;
?>
<style>
.catalog-wrapper { display: flex; gap: 30px; margin-top: 20px; }
.catalog-sidebar { width: 250px; flex-shrink: 0; }
.catalog-products { flex: 1; }
.filter-group { margin-bottom: 25px; }
.filter-title { color: #453227; font-size: 16px; font-weight: bold; margin-bottom: 15px; border-bottom: 2px solid #453227; padding-bottom: 5px; }
.filter-list { list-style: none; padding: 0; margin: 0; }
.filter-list li { margin-bottom: 8px; }
.filter-list a { color: #453227; text-decoration: none; font-size: 14px; transition: all 0.3s ease; }
.filter-list a:hover { color: #617365; padding-left: 5px; }
.active-category { font-weight: bold !important; color: #617365 !important; }
.products-container { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 20px; }
.product-card { background: #f5f5f5; border-radius: 8px; overflow: hidden; position: relative; cursor: pointer; transition: transform 0.3s; }
.product-card:hover { transform: translateY(-5px); }
.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 .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; }
.admin-panel { background: #f8f9fa; padding: 15px; margin-bottom: 20px; border-radius: 8px; border-left: 4px solid #617365; }
.admin-btn { background: #617365; color: white; padding: 8px 15px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; margin: 0 5px 5px 0; text-decoration: none; display: inline-block; }
.admin-btn:hover { background: #453227; color: white; }
.user-welcome { background: #f5e9dc; color: #453227; padding: 15px; border-radius: 8px; margin-bottom: 20px; border-left: 4px solid #453227; }
.product-card.unavailable { opacity: 0.6; filter: grayscale(0.7); }
</style>
<main class="catalog-main">
<div class="container">
<div class="breadcrumbs">
<a href="/cite_practica/">Главная</a> • <span class="current-page">Каталог</span>
</div>
<?php if (!empty($success)): ?>
<div style="background: #d4edda; color: #155724; padding: 15px; border-radius: 5px; margin: 20px 0;">
<?= htmlspecialchars($success) ?>
</div>
<?php endif; ?>
<?php if ($isAdmin): ?>
<div class="admin-panel">
<h3 style="margin-bottom: 15px; color: #453227;">
<i class="fas fa-user-shield"></i> Панель управления каталогом
</h3>
<div>
<a href="/cite_practica/admin/products" class="admin-btn"><i class="fas fa-boxes"></i> Управление каталогом</a>
<a href="/cite_practica/admin/products/add" class="admin-btn"><i class="fas fa-plus"></i> Добавить товар</a>
<a href="/cite_practica/admin/categories" class="admin-btn"><i class="fas fa-tags"></i> Категории</a>
<a href="/cite_practica/admin/orders" class="admin-btn"><i class="fas fa-shopping-cart"></i> Заказы</a>
</div>
</div>
<?php endif; ?>
<div class="user-welcome">
<i class="fas fa-user-check"></i> Добро пожаловать, <strong><?= htmlspecialchars($user['full_name'] ?? $user['email']) ?></strong>!
<?php if ($isAdmin): ?>
<span style="background: #453227; color: white; padding: 3px 8px; border-radius: 4px; font-size: 12px; margin-left: 10px;">
<i class="fas fa-user-shield"></i> Администратор
</span>
<?php endif; ?>
</div>
<div class="catalog-wrapper">
<aside class="catalog-sidebar">
<form method="GET" action="/cite_practica/catalog" id="filterForm">
<div class="filter-group">
<h4 class="filter-title">КАТЕГОРИИ</h4>
<ul class="filter-list">
<li><a href="/cite_practica/catalog" class="<?= empty($filters['category_id']) ? 'active-category' : '' ?>">Все товары</a></li>
<?php foreach ($categories as $category): ?>
<li>
<a href="/cite_practica/catalog?category=<?= $category['category_id'] ?>"
class="<?= $filters['category_id'] == $category['category_id'] ? 'active-category' : '' ?>">
<?= htmlspecialchars($category['name']) ?>
</a>
</li>
<?php endforeach; ?>
</ul>
</div>
<div class="filter-group">
<h4 class="filter-title">СТОИМОСТЬ</h4>
<div class="price-range">
<input type="range" min="0" max="100000" value="<?= $filters['max_price'] ?>"
step="1000" id="priceSlider" name="max_price" style="width: 100%;">
<div id="priceDisplay" style="text-align: center; margin-top: 10px; font-weight: bold; color: #453227;">
До <?= number_format($filters['max_price'], 0, '', ' ') ?> ₽
</div>
</div>
</div>
<?php if (!empty($availableColors)): ?>
<div class="filter-group">
<h4 class="filter-title">ЦВЕТ</h4>
<ul class="filter-list">
<?php foreach ($availableColors as $color): ?>
<li>
<label>
<input type="checkbox" name="colors[]" value="<?= htmlspecialchars($color) ?>"
<?= in_array($color, $filters['colors']) ? 'checked' : '' ?>>
<?= htmlspecialchars($color) ?>
</label>
</li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<button type="submit" style="width: 100%; background: #453227; color: white; padding: 12px; border: none; border-radius: 4px; cursor: pointer; font-weight: bold;">
ПРИМЕНИТЬ ФИЛЬТРЫ
</button>
</form>
</aside>
<section class="catalog-products">
<div style="margin-bottom: 20px;">
<h2 style="color: #453227; margin-bottom: 10px;">
Каталог мебели
<span style="font-size: 14px; color: #666; font-weight: normal;">
(<?= count($products) ?> товаров)
</span>
</h2>
<?php if (!empty($filters['search'])): ?>
<p style="color: #666;">
Результаты поиска: "<strong><?= htmlspecialchars($filters['search']) ?></strong>"
<a href="/cite_practica/catalog" style="margin-left: 10px; color: #617365;">
<i class="fas fa-times"></i> Очистить
</a>
</p>
<?php endif; ?>
</div>
<div class="products-container">
<?php if (empty($products)): ?>
<p style="grid-column: 1/-1; text-align: center; padding: 40px; color: #666;">
Товары не найдены
</p>
<?php else: ?>
<?php foreach ($products as $product): ?>
<div class="product-card <?= !$product['is_available'] ? 'unavailable' : '' ?>"
onclick="window.location.href='/cite_practica/product/<?= $product['product_id'] ?>'"
data-product-id="<?= $product['product_id'] ?>">
<img src="/cite_practica/<?= htmlspecialchars($product['image_url'] ?? 'img/1.jpg') ?>"
alt="<?= htmlspecialchars($product['name']) ?>">
<div class="product-info">
<div class="name"><?= htmlspecialchars($product['name']) ?></div>
<div class="price"><?= View::formatPrice($product['price']) ?></div>
</div>
<?php if ($product['is_available']): ?>
<i class="fas fa-shopping-cart add-to-cart-btn"
onclick="event.stopPropagation(); addToCart(<?= $product['product_id'] ?>, '<?= addslashes($product['name']) ?>')"
title="Добавить в корзину"></i>
<?php endif; ?>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</section>
</div>
</div>
</main>
<script>
$('#priceSlider').on('input', function() {
const value = $(this).val();
$('#priceDisplay').text('До ' + new Intl.NumberFormat('ru-RU').format(value) + ' ₽');
});
function addToCart(productId, productName) {
$.ajax({
url: '/cite_practica/cart/add',
method: 'POST',
data: { product_id: productId, quantity: 1 },
dataType: 'json',
success: function(result) {
if (result.success) {
showNotification('Товар "' + productName + '" добавлен в корзину!');
$('.cart-count').text(result.cart_count);
} else {
showNotification('Ошибка: ' + result.message, 'error');
}
},
error: function() {
showNotification('Ошибка сервера', 'error');
}
});
}
</script>

210
app/Views/products/show.php Normal file
View File

@@ -0,0 +1,210 @@
<?php
$title = $product['name'];
use App\Core\View;
?>
<style>
.product__section { display: grid; grid-template-columns: 350px 1fr; gap: 30px; margin: 30px 0; }
.product__main-image { width: 350px; height: 350px; background: #f8f9fa; border-radius: 8px; overflow: hidden; display: flex; align-items: center; justify-content: center; }
.product__main-image img { width: 100%; height: 100%; object-fit: contain; }
.product__info h1 { color: #453227; margin-bottom: 15px; }
.product__price { margin: 20px 0; }
.current-price { font-size: 28px; font-weight: bold; color: #453227; }
.old-price { font-size: 18px; color: #999; text-decoration: line-through; margin-left: 15px; }
.discount-badge { background: #dc3545; color: white; padding: 5px 10px; border-radius: 4px; font-size: 14px; margin-left: 10px; }
.stock-status { display: inline-block; padding: 8px 15px; border-radius: 4px; font-weight: bold; margin: 15px 0; }
.in-stock { background: #d4edda; color: #155724; }
.low-stock { background: #fff3cd; color: #856404; }
.out-of-stock { background: #f8d7da; color: #721c24; }
.product-attributes { background: #f8f9fa; padding: 20px; border-radius: 8px; margin: 20px 0; }
.attribute-row { display: flex; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid #ddd; }
.attribute-label { font-weight: bold; color: #453227; }
.attribute-value { color: #617365; }
.product__purchase { display: flex; gap: 15px; align-items: center; margin: 20px 0; }
.product__quantity { display: flex; align-items: center; gap: 10px; }
.product__qty-btn { width: 35px; height: 35px; border: 1px solid #ddd; background: white; cursor: pointer; font-size: 18px; border-radius: 4px; }
.product__qty-value { width: 50px; text-align: center; border: 1px solid #ddd; padding: 8px; border-radius: 4px; }
.product__actions { display: flex; gap: 10px; }
.similar-products { margin: 40px 0; }
.similar-products h2 { color: #453227; margin-bottom: 20px; }
.products-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; }
.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; }
</style>
<main class="container">
<div class="breadcrumbs">
<a href="/cite_practica/">Главная</a> •
<a href="/cite_practica/catalog">Каталог</a> •
<?php if ($product['category_name']): ?>
<a href="/cite_practica/catalog?category=<?= $product['category_id'] ?>">
<?= htmlspecialchars($product['category_name']) ?>
</a> •
<?php endif; ?>
<span><?= htmlspecialchars($product['name']) ?></span>
</div>
<div class="product__section">
<div class="product__gallery">
<div class="product__main-image">
<img src="/cite_practica/<?= htmlspecialchars($product['image_url'] ?? 'img/1.jpg') ?>"
alt="<?= htmlspecialchars($product['name']) ?>">
</div>
</div>
<div class="product__info">
<h1><?= htmlspecialchars($product['name']) ?></h1>
<div class="product__rating">
<div class="stars">
<?php
$rating = $product['rating'] ?? 0;
for ($i = 1; $i <= 5; $i++) {
echo $i <= $rating ? '<span class="star filled">★</span>' : '<span class="star">☆</span>';
}
?>
</div>
<span>(<?= $product['review_count'] ?? 0 ?> отзывов)</span>
</div>
<div class="product__price">
<span class="current-price"><?= View::formatPrice($product['price']) ?></span>
<?php if ($product['old_price'] && $product['old_price'] > $product['price']): ?>
<span class="old-price"><?= View::formatPrice($product['old_price']) ?></span>
<span class="discount-badge">
-<?= round(($product['old_price'] - $product['price']) / $product['old_price'] * 100) ?>%
</span>
<?php endif; ?>
</div>
<div class="stock-status <?php
if ($product['stock_quantity'] > 10) echo 'in-stock';
elseif ($product['stock_quantity'] > 0) echo 'low-stock';
else echo 'out-of-stock';
?>">
<?php
if ($product['stock_quantity'] > 10) {
echo '<i class="fas fa-check-circle"></i> В наличии';
} elseif ($product['stock_quantity'] > 0) {
echo '<i class="fas fa-exclamation-circle"></i> Осталось мало: ' . $product['stock_quantity'] . ' шт.';
} else {
echo '<i class="fas fa-times-circle"></i> Нет в наличии';
}
?>
</div>
<div class="product-attributes">
<div class="attribute-row">
<span class="attribute-label">Артикул:</span>
<span class="attribute-value"><?= $product['sku'] ?? 'N/A' ?></span>
</div>
<div class="attribute-row">
<span class="attribute-label">Категория:</span>
<span class="attribute-value"><?= htmlspecialchars($product['category_name'] ?? 'Без категории') ?></span>
</div>
<div class="attribute-row">
<span class="attribute-label">На складе:</span>
<span class="attribute-value"><?= $product['stock_quantity'] ?> шт.</span>
</div>
</div>
<p class="product__description">
<?= nl2br(htmlspecialchars($product['description'] ?? 'Описание отсутствует')) ?>
</p>
<?php if ($product['stock_quantity'] > 0): ?>
<div class="product__purchase">
<div class="product__quantity">
<button class="product__qty-btn minus">-</button>
<input type="number" class="product__qty-value" value="1" min="1" max="<?= $product['stock_quantity'] ?>">
<button class="product__qty-btn plus">+</button>
</div>
<div class="product__actions">
<button class="btn primary-btn" onclick="addToCart(<?= $product['product_id'] ?>)">
<i class="fas fa-shopping-cart"></i> В корзину
</button>
<button class="btn secondary-btn" onclick="buyNow(<?= $product['product_id'] ?>)">
<i class="fas fa-bolt"></i> Купить сейчас
</button>
</div>
</div>
<?php endif; ?>
<?php if ($isAdmin): ?>
<div style="margin-top: 20px;">
<a href="/cite_practica/admin/products/edit/<?= $product['product_id'] ?>" class="btn" style="background: #ffc107; color: #333;">
<i class="fas fa-edit"></i> Редактировать
</a>
</div>
<?php endif; ?>
</div>
</div>
<?php if (!empty($similarProducts)): ?>
<section class="similar-products">
<h2>Похожие товары</h2>
<div class="products-grid">
<?php foreach ($similarProducts as $similar): ?>
<div class="product-card" onclick="window.location.href='/cite_practica/product/<?= $similar['product_id'] ?>'" style="cursor: pointer;">
<img src="/cite_practica/<?= htmlspecialchars($similar['image_url'] ?? 'img/1.jpg') ?>"
alt="<?= htmlspecialchars($similar['name']) ?>">
<div class="product-info">
<h3 style="font-size: 16px; color: #453227;"><?= htmlspecialchars($similar['name']) ?></h3>
<p style="color: #617365; font-weight: bold;"><?= View::formatPrice($similar['price']) ?></p>
</div>
</div>
<?php endforeach; ?>
</div>
</section>
<?php endif; ?>
</main>
<script>
$(document).ready(function() {
$('.product__qty-btn.plus').click(function() {
const $input = $('.product__qty-value');
let value = parseInt($input.val());
let max = parseInt($input.attr('max'));
if (value < max) $input.val(value + 1);
});
$('.product__qty-btn.minus').click(function() {
const $input = $('.product__qty-value');
let value = parseInt($input.val());
if (value > 1) $input.val(value - 1);
});
});
function addToCart(productId) {
const quantity = $('.product__qty-value').val();
$.ajax({
url: '/cite_practica/cart/add',
method: 'POST',
data: { product_id: productId, quantity: quantity },
dataType: 'json',
success: function(result) {
if (result.success) {
showNotification('Товар добавлен в корзину!');
$('.cart-count').text(result.cart_count);
} else {
showNotification('Ошибка: ' + result.message, 'error');
}
}
});
}
function buyNow(productId) {
const quantity = $('.product__qty-value').val();
$.ajax({
url: '/cite_practica/cart/add',
method: 'POST',
data: { product_id: productId, quantity: quantity },
success: function() {
window.location.href = '/cite_practica/cart';
}
});
}
</script>