[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

304
app/Views/cart/checkout.php Normal file
View File

@@ -0,0 +1,304 @@
<?php
$title = 'Корзина';
use App\Core\View;
?>
<style>
.main__content { display: flex; gap: 30px; margin: 30px 0; }
.products { flex: 1; }
.order { width: 400px; background: #f8f9fa; padding: 25px; border-radius: 8px; height: fit-content; }
.products__list { display: flex; flex-direction: column; gap: 15px; }
.products__item { display: flex; gap: 15px; background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.05); }
.products__image { width: 100px; height: 100px; flex-shrink: 0; }
.products__image img { width: 100%; height: 100%; object-fit: cover; border-radius: 4px; }
.products__details { flex: 1; display: flex; flex-direction: column; justify-content: space-between; }
.products__name { font-weight: bold; color: #453227; }
.products__price { color: #617365; font-size: 18px; font-weight: bold; }
.products__controls { display: flex; align-items: center; gap: 15px; }
.products__quantity { display: flex; align-items: center; gap: 8px; }
.products__qty-btn { width: 30px; height: 30px; border: 1px solid #ddd; background: white; cursor: pointer; border-radius: 4px; }
.products__qty-value { padding: 5px 10px; }
.products__cart-icon { background: none; border: none; color: #dc3545; cursor: pointer; font-size: 18px; }
.empty-cart { text-align: center; padding: 40px; color: #666; }
.order__title { color: #453227; margin-bottom: 20px; }
.order__section { margin-bottom: 20px; }
.order__section-title { color: #453227; font-size: 14px; margin-bottom: 10px; }
.form__radio-group { display: flex; flex-direction: column; gap: 10px; margin-bottom: 15px; }
.form__radio-label { display: flex; align-items: center; gap: 8px; cursor: pointer; }
.form__input { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; margin-bottom: 10px; }
.form__row { display: flex; gap: 10px; }
.summary { margin: 20px 0; }
.summary__item { display: flex; justify-content: space-between; padding: 8px 0; }
.summary__item.total { font-weight: bold; font-size: 18px; border-top: 2px solid #453227; margin-top: 10px; padding-top: 15px; }
.order-btn { width: 100%; background: #453227; color: white; padding: 15px; border: none; border-radius: 4px; font-size: 16px; font-weight: bold; cursor: pointer; }
.order-btn:hover { background: #617365; }
.promo { display: flex; gap: 10px; margin-bottom: 20px; }
.promo__input { flex: 1; padding: 10px; border: 1px solid #ddd; border-radius: 4px; }
.promo__btn { background: #617365; color: white; border: none; padding: 10px 15px; border-radius: 4px; cursor: pointer; }
.privacy { display: flex; align-items: center; gap: 8px; margin-top: 15px; font-size: 14px; color: #666; }
</style>
<main class="container">
<div class="breadcrumbs">
<a href="/cite_practica/">Главная</a> • <span class="current-page">Корзина</span>
</div>
<h2 style="color: #453227; margin: 20px 0;">Товары в корзине</h2>
<?php if (empty($cartItems)): ?>
<div class="empty-cart">
<i class="fas fa-shopping-cart" style="font-size: 48px; color: #ccc; margin-bottom: 20px;"></i>
<p>Ваша корзина пуста</p>
<a href="/cite_practica/catalog" class="btn primary-btn" style="margin-top: 20px; display: inline-block; background: #453227; color: white; padding: 12px 30px; border-radius: 4px; text-decoration: none;">
Продолжить покупки
</a>
</div>
<?php else: ?>
<div class="main__content">
<section class="products">
<div class="products__list" id="cartItems">
<?php foreach ($cartItems as $item): ?>
<div class="products__item" data-product-id="<?= $item['product_id'] ?>" data-price="<?= $item['price'] ?>">
<div class="products__image">
<img src="/cite_practica/<?= htmlspecialchars($item['image_url'] ?? 'img/1.jpg') ?>"
alt="<?= htmlspecialchars($item['name']) ?>">
</div>
<div class="products__details">
<div class="products__name"><?= htmlspecialchars($item['name']) ?></div>
<div class="products__price"><?= View::formatPrice($item['price']) ?></div>
<div class="products__controls">
<div class="products__quantity">
<button class="products__qty-btn minus" data-id="<?= $item['product_id'] ?>">-</button>
<span class="products__qty-value"><?= $item['quantity'] ?></span>
<button class="products__qty-btn plus" data-id="<?= $item['product_id'] ?>">+</button>
</div>
<button class="products__cart-icon remove-from-cart" data-id="<?= $item['product_id'] ?>">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
</section>
<section class="order">
<form id="orderForm">
<h2 class="order__title">Оформление заказа</h2>
<div class="order__section">
<h3 class="order__section-title">СПОСОБ ДОСТАВКИ</h3>
<div class="form__radio-group">
<label class="form__radio-label">
<input type="radio" name="delivery" value="courier" checked>
Курьерская доставка
</label>
<label class="form__radio-label">
<input type="radio" name="delivery" value="pickup">
Самовывоз
</label>
</div>
<input type="text" name="full_name" class="form__input" placeholder="ФИО..."
value="<?= htmlspecialchars($user['full_name'] ?? '') ?>" required>
<div class="form__row">
<input type="tel" name="phone" class="form__input" placeholder="Телефон..."
value="<?= htmlspecialchars($user['phone'] ?? '') ?>" required style="flex: 1;">
<input type="email" name="email" class="form__input" placeholder="E-mail..."
value="<?= htmlspecialchars($user['email'] ?? '') ?>" required style="flex: 1;">
</div>
</div>
<div class="order__section">
<h3 class="order__section-title">СПОСОБ ОПЛАТЫ</h3>
<div class="form__radio-group">
<label class="form__radio-label">
<input type="radio" name="payment" value="card" checked>
Банковская карта
</label>
<label class="form__radio-label">
<input type="radio" name="payment" value="cash">
Наличные
</label>
</div>
<div class="form__row">
<input type="text" name="region" class="form__input" placeholder="Регион..." required style="flex: 1;">
<input type="text" name="postal_code" class="form__input" placeholder="Индекс..." style="flex: 1;">
</div>
<input type="text" name="address" class="form__input" placeholder="Улица, дом, квартира..." required>
</div>
<div class="promo">
<input type="text" id="promo_code" name="promo_code" class="promo__input" placeholder="Промокод">
<button type="button" class="promo__btn" id="applyPromo">ПРИМЕНИТЬ</button>
</div>
<input type="hidden" name="discount" value="0">
<input type="hidden" name="delivery_price" value="2000">
<div class="summary">
<div class="summary__item">
<span>Товары, <span class="summary-count"><?= $totalQuantity ?></span> шт.</span>
<span class="products-total"><?= View::formatPrice($totalAmount) ?></span>
</div>
<div class="summary__item">
<span>Скидка</span>
<span class="discount-total">0 ₽</span>
</div>
<div class="summary__item">
<span>Доставка</span>
<span class="delivery-price">2 000 ₽</span>
</div>
<div class="summary__item total">
<span>ИТОГО:</span>
<span class="final-total"><?= View::formatPrice($totalAmount + 2000) ?></span>
</div>
</div>
<button type="submit" class="order-btn" id="submit-order">ОФОРМИТЬ ЗАКАЗ</button>
<label class="privacy">
<input type="checkbox" id="privacy-checkbox" required>
Даю согласие на обработку персональных данных
</label>
</form>
</section>
</div>
<?php endif; ?>
</main>
<script>
$(document).ready(function() {
// Обновление количества
$('.products__qty-btn').on('click', function(e) {
e.preventDefault();
const productId = $(this).data('id');
const isPlus = $(this).hasClass('plus');
const $qtyValue = $(this).siblings('.products__qty-value');
let quantity = parseInt($qtyValue.text());
if (isPlus) { quantity++; }
else if (quantity > 1) { quantity--; }
else { return; }
$qtyValue.text(quantity);
$.ajax({
url: '/cite_practica/cart/update',
method: 'POST',
data: { product_id: productId, quantity: quantity },
dataType: 'json',
success: function(result) {
if (result.success) {
updateTotals();
$('.cart-count').text(result.cart_count);
}
}
});
});
// Удаление товара
$('.remove-from-cart').on('click', function(e) {
e.preventDefault();
const productId = $(this).data('id');
const $item = $(this).closest('.products__item');
if (!confirm('Удалить товар из корзины?')) return;
$.ajax({
url: '/cite_practica/cart/remove',
method: 'POST',
data: { product_id: productId },
dataType: 'json',
success: function(result) {
if (result.success) {
$item.fadeOut(300, function() {
$(this).remove();
updateTotals();
$('.cart-count').text(result.cart_count);
if ($('#cartItems .products__item').length === 0) {
location.reload();
}
});
}
}
});
});
function updateTotals() {
let productsTotal = 0;
let totalCount = 0;
$('.products__item').each(function() {
const price = parseInt($(this).data('price'));
const quantity = parseInt($(this).find('.products__qty-value').text());
productsTotal += price * quantity;
totalCount += quantity;
});
const delivery = parseFloat($('input[name="delivery_price"]').val());
const discount = parseFloat($('input[name="discount"]').val());
const finalTotal = productsTotal + delivery - discount;
$('.products-total').text(productsTotal.toLocaleString('ru-RU') + ' ₽');
$('.summary-count').text(totalCount);
$('.final-total').text(finalTotal.toLocaleString('ru-RU') + ' ₽');
}
// Промокод
$('#applyPromo').click(function() {
const promoCode = $('#promo_code').val().toUpperCase();
if (promoCode === 'SALE10') {
const productsTotal = parseFloat($('.products-total').text().replace(/[^0-9]/g, ''));
const discount = Math.round(productsTotal * 0.1);
$('input[name="discount"]').val(discount);
$('.discount-total').text(discount.toLocaleString('ru-RU') + ' ₽');
showNotification('Промокод применен! Скидка 10%');
updateTotals();
} else if (promoCode === 'FREE') {
$('input[name="delivery_price"]').val(0);
$('.delivery-price').text('0 ₽');
showNotification('Промокод применен! Бесплатная доставка');
updateTotals();
} else if (promoCode) {
showNotification('Промокод недействителен', 'error');
}
});
// Оформление заказа
$('#orderForm').submit(function(e) {
e.preventDefault();
if (!$('#privacy-checkbox').is(':checked')) {
showNotification('Необходимо согласие на обработку данных', 'error');
return;
}
$('#submit-order').prop('disabled', true).text('ОБРАБОТКА...');
$.ajax({
url: '/cite_practica/order',
method: 'POST',
data: $(this).serialize(),
dataType: 'json',
success: function(result) {
if (result.success) {
showNotification('Заказ успешно оформлен!');
setTimeout(function() {
window.location.href = '/cite_practica/';
}, 1500);
} else {
showNotification('Ошибка: ' + result.message, 'error');
$('#submit-order').prop('disabled', false).text('ОФОРМИТЬ ЗАКАЗ');
}
},
error: function() {
showNotification('Ошибка сервера', 'error');
$('#submit-order').prop('disabled', false).text('ОФОРМИТЬ ЗАКАЗ');
}
});
});
});
</script>