Исправление багов авторизации, корзины и админки

- Исправлено выпадающее меню профиля (hover-баг с margin-top)
- Исправлена авторизация: правильные пути к API (api/auth.php)
- Исправлены ссылки на админку (admin/index.php вместо admin_panel.php)
- Исправлены пути API корзины в catalog.php и checkout.php
- Добавлена форма добавления/редактирования товаров в админке
- Исправлены кнопки +/- в корзине (улучшена обработка AJAX)
- Исправлена регистрация: правильные пути и обработка boolean в PostgreSQL
- Добавлена миграция для назначения прав админа пользователю admin@mail.ru
- Удален тестовый блок 'Быстрый вход' для неавторизованных пользователей
- Улучшена обработка ошибок во всех API-эндпоинтах
This commit is contained in:
kirill.khorkov
2025-12-16 02:58:44 +03:00
parent 3f257120fa
commit 29b9aaac50
388 changed files with 15312 additions and 3220 deletions

816
public/admin/index.php Normal file
View File

@@ -0,0 +1,816 @@
<?php
// index.php - ПОЛНОСТЬЮ ИСПРАВЛЕННАЯ ВЕРСИЯ
session_start();
require_once __DIR__ . '/../config/database.php';
// Включаем отладку ошибок
error_reporting(E_ALL);
ini_set('display_errors', 1);
// Проверка прав администратора
if (!isset($_SESSION['isAdmin']) || $_SESSION['isAdmin'] !== true) {
echo "<script>alert('Требуется авторизация администратора'); window.location.href = '../login.php';</script>";
exit();
}
$db = Database::getInstance()->getConnection();
// Обработка действий
$action = $_GET['action'] ?? 'dashboard';
$message = $_GET['message'] ?? '';
$error = $_GET['error'] ?? '';
// Обработка POST запросов - ДОБАВЛЕНО ПРОСТОЕ И РАБОТАЮЩЕЕ!
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$post_action = $_POST['action'] ?? '';
try {
if ($post_action === 'add_category') {
$name = trim($_POST['name'] ?? '');
$slug = strtolower(preg_replace('/[^a-z0-9]+/i', '-', $name));
$parent_id = !empty($_POST['parent_id']) ? (int)$_POST['parent_id'] : NULL;
$description = trim($_POST['description'] ?? '');
$sort_order = (int)($_POST['sort_order'] ?? 0);
$is_active = isset($_POST['is_active']) ? 1 : 0;
if (empty($name)) {
throw new Exception('Название категории обязательно');
}
$stmt = $db->prepare("
INSERT INTO categories (name, slug, parent_id, description, sort_order, is_active)
VALUES (?, ?, ?, ?, ?, ?)
");
$result = $stmt->execute([$name, $slug, $parent_id, $description, $sort_order, $is_active]);
if ($result) {
header('Location: index.php?action=categories&message=Категория+успешно+добавлена');
exit();
}
}
// ИСПРАВЬТЕ БЛОК edit_category или добавьте его если его нет:
if ($post_action === 'edit_category' && isset($_POST['category_id'])) {
$category_id = (int)$_POST['category_id'];
$name = trim($_POST['name'] ?? '');
$parent_id = !empty($_POST['parent_id']) ? (int)$_POST['parent_id'] : NULL;
$description = trim($_POST['description'] ?? '');
$sort_order = (int)($_POST['sort_order'] ?? 0);
$is_active = isset($_POST['is_active']) ? 1 : 0;
if (empty($name)) {
throw new Exception('Название категории обязательно');
}
$slug = strtolower(preg_replace('/[^a-z0-9]+/i', '-', $name));
$stmt = $db->prepare("
UPDATE categories SET
name = ?,
slug = ?,
parent_id = ?,
description = ?,
sort_order = ?,
is_active = ?,
updated_at = CURRENT_TIMESTAMP
WHERE category_id = ?
");
$stmt->execute([$name, $slug, $parent_id, $description, $sort_order, $is_active, $category_id]);
header('Location: index.php?action=categories&message=Категория+обновлена');
exit();
}
if ($post_action === 'add_product') {
$name = trim($_POST['name'] ?? '');
$slug = strtolower(preg_replace('/[^a-z0-9]+/i', '-', $name));
$category_id = (int)($_POST['category_id'] ?? 0);
$description = trim($_POST['description'] ?? '');
$price = (float)($_POST['price'] ?? 0);
$old_price = !empty($_POST['old_price']) ? (float)$_POST['old_price'] : NULL;
$sku = trim($_POST['sku'] ?? '');
$stock_quantity = (int)($_POST['stock_quantity'] ?? 0);
$is_available = isset($_POST['is_available']) ? 1 : 0;
$is_featured = isset($_POST['is_featured']) ? 1 : 0;
$image_url = trim($_POST['image_url'] ?? '');
$color = trim($_POST['color'] ?? '');
$material = trim($_POST['material'] ?? '');
$card_size = trim($_POST['card_size'] ?? 'small');
// ВАЖНО: Проверяем category_id
if ($category_id <= 0) {
$_SESSION['error'] = 'Выберите корректную категорию';
header('Location: index.php?action=add_product');
exit();
}
// Проверяем существование категории
$check_category = $db->prepare("SELECT COUNT(*) FROM categories WHERE category_id = ?");
$check_category->execute([$category_id]);
if ($check_category->fetchColumn() == 0) {
$_SESSION['error'] = 'Выбранная категория не существует';
header('Location: index.php?action=add_product');
exit();
}
if (empty($name)) throw new Exception('Название товара обязательно');
if ($price <= 0) throw new Exception('Цена должна быть больше 0');
// Генерируем SKU если пустой
if (empty($sku)) {
$sku = 'PROD-' . strtoupper(substr(preg_replace('/[^a-z0-9]/i', '', $name), 0, 6)) . '-' . rand(100, 999);
}
$stmt = $db->prepare("
INSERT INTO products (
category_id, name, slug, description, price, old_price,
sku, stock_quantity, is_available, is_featured, image_url,
color, material, card_size
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
");
$result = $stmt->execute([
$category_id, $name, $slug, $description, $price, $old_price,
$sku, $stock_quantity, $is_available, $is_featured, $image_url,
$color, $material, $card_size
]);
if ($result) {
$_SESSION['message'] = 'Товар успешно добавлен';
header('Location: index.php?action=products');
exit();
}
}
// ИСПРАВЛЕННЫЙ КОД для edit_product в index.php:
if ($post_action === 'edit_product' && isset($_POST['product_id'])) {
$product_id = (int)$_POST['product_id'];
$name = trim($_POST['name'] ?? '');
$category_id = (int)($_POST['category_id'] ?? 1); // ПО УМОЛЧАНИЮ 1, чтобы избежать 0
$description = trim($_POST['description'] ?? '');
$price = (float)($_POST['price'] ?? 0);
$old_price = !empty($_POST['old_price']) ? (float)$_POST['old_price'] : NULL;
$stock_quantity = (int)($_POST['stock_quantity'] ?? 0);
$is_available = isset($_POST['is_available']) ? 1 : 0;
$image_url = trim($_POST['image_url'] ?? '');
$color = trim($_POST['color'] ?? '');
$material = trim($_POST['material'] ?? '');
// ВАЖНО: Проверяем category_id
if ($category_id <= 0) {
// Если category_id = 0, устанавливаем первую доступную категорию
$firstCat = $db->query("SELECT category_id FROM categories LIMIT 1")->fetchColumn();
$category_id = $firstCat ?: 1;
}
$stmt = $db->prepare("
UPDATE products SET
name = ?,
category_id = ?,
description = ?,
price = ?,
old_price = ?,
stock_quantity = ?,
is_available = ?,
image_url = ?,
color = ?,
material = ?,
updated_at = CURRENT_TIMESTAMP
WHERE product_id = ?
");
$stmt->execute([
$name, $category_id, $description, $price, $old_price,
$stock_quantity, $is_available, $image_url, $color, $material, $product_id
]);
header('Location: index.php?action=products&message=Товар+обновлен');
exit();
}
if ($post_action === 'delete_category' && isset($_POST['category_id'])) {
$categoryId = intval($_POST['category_id']);
// 1. Проверяем, есть ли товары в этой категории
$checkProducts = $db->prepare("SELECT COUNT(*) FROM products WHERE category_id = ?");
$checkProducts->execute([$categoryId]);
$productCount = $checkProducts->fetchColumn();
// 2. Проверяем, есть ли дочерние категории
$checkChildren = $db->prepare("SELECT COUNT(*) FROM categories WHERE parent_id = ?");
$checkChildren->execute([$categoryId]);
$childCount = $checkChildren->fetchColumn();
if ($productCount > 0) {
// Если есть товары, делаем категорию неактивной вместо удаления
$stmt = $db->prepare("UPDATE categories SET is_active = FALSE WHERE category_id = ?");
$stmt->execute([$categoryId]);
header('Location: index.php?action=categories&message=Категория+скрыта+(содержит+товары)');
exit();
} elseif ($childCount > 0) {
// Если есть дочерние категории, делаем неактивной
$stmt = $db->prepare("UPDATE categories SET is_active = FALSE WHERE category_id = ?");
$stmt->execute([$categoryId]);
header('Location: index.php?action=categories&message=Категория+скрыта+(имеет+дочерние+категории)');
exit();
} else {
// Если нет товаров и дочерних категорий, удаляем
$stmt = $db->prepare("DELETE FROM categories WHERE category_id = ?");
$stmt->execute([$categoryId]);
header('Location: index.php?action=categories&message=Категория+удалена');
exit();
}
}
} catch (PDOException $e) {
header('Location: index.php?action=' . $action . '&error=' . urlencode('Ошибка БД: ' . $e->getMessage()));
exit();
} catch (Exception $e) {
header('Location: index.php?action=' . $action . '&error=' . urlencode($e->getMessage()));
exit();
}
}
// Получение данных для отображения
try {
// Статистика
$stats = [
'total_products' => $db->query("SELECT COUNT(*) FROM products")->fetchColumn(),
'active_products' => $db->query("SELECT COUNT(*) FROM products WHERE is_available = TRUE")->fetchColumn(),
'total_orders' => $db->query("SELECT COUNT(*) FROM orders")->fetchColumn(),
'total_users' => $db->query("SELECT COUNT(*) FROM users")->fetchColumn(),
'revenue' => $db->query("SELECT COALESCE(SUM(final_amount), 0) FROM orders WHERE status = 'completed'")->fetchColumn()
];
// Получаем все категории
$allCategories = $db->query("SELECT * FROM categories WHERE is_active = TRUE ORDER BY name")->fetchAll();
// Получаем родительские категории
$parentCategories = $db->query("SELECT * FROM categories WHERE parent_id IS NULL AND is_active = TRUE ORDER BY name")->fetchAll();
switch ($action) {
case 'products':
$showAll = isset($_GET['show_all']) && $_GET['show_all'] == '1';
$sql = $showAll
? "SELECT p.*, c.name as category_name FROM products p LEFT JOIN categories c ON p.category_id = c.category_id ORDER BY p.created_at DESC"
: "SELECT p.*, c.name as category_name FROM products p LEFT JOIN categories c ON p.category_id = c.category_id WHERE p.is_available = TRUE ORDER BY p.created_at DESC";
$data = $db->query($sql)->fetchAll();
break;
case 'categories':
$data = $db->query("
SELECT c1.*, c2.name as parent_name,
(SELECT COUNT(*) FROM products p WHERE p.category_id = c1.category_id) as product_count
FROM categories c1
LEFT JOIN categories c2 ON c1.parent_id = c2.category_id
ORDER BY c1.sort_order, c1.name
")->fetchAll();
break;
case 'orders':
$data = $db->query("
SELECT o.*, u.email as user_email
FROM orders o
LEFT JOIN users u ON o.user_id = u.user_id
ORDER BY o.created_at DESC
LIMIT 50
")->fetchAll();
break;
case 'users':
$data = $db->query("SELECT * FROM users ORDER BY created_at DESC LIMIT 50")->fetchAll();
break;
case 'add_product':
case 'edit_product':
if ($action === 'edit_product' && isset($_GET['id'])) {
$productId = (int)$_GET['id'];
$stmt = $db->prepare("SELECT * FROM products WHERE product_id = ?");
$stmt->execute([$productId]);
$edit_data = $stmt->fetch();
}
break;
case 'add_category':
case 'edit_category':
if ($action === 'edit_category' && isset($_GET['id'])) {
$categoryId = (int)$_GET['id'];
$stmt = $db->prepare("SELECT * FROM categories WHERE category_id = ?");
$stmt->execute([$categoryId]);
$edit_data = $stmt->fetch();
}
break;
}
} catch (PDOException $e) {
$error = "Ошибка базы данных: " . $e->getMessage();
}
?>
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AETERNA - Админ-панель</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
<style>
body { font-family: Arial, sans-serif; margin: 0; padding: 0; background: #f5f5f5; }
.admin-header { background: #453227; color: white; padding: 20px; display: flex; justify-content: space-between; align-items: center; }
.admin-tabs { background: white; padding: 10px; border-bottom: 2px solid #453227; display: flex; gap: 10px; }
.admin-tab { padding: 10px 20px; border-radius: 5px; text-decoration: none; color: #333; }
.admin-tab:hover, .admin-tab.active { background: #453227; color: white; }
.admin-content { padding: 20px; }
.form-container { background: white; padding: 20px; border-radius: 5px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); max-width: 800px; margin: 0 auto; }
.form-group { margin-bottom: 15px; }
.form-group label { display: block; margin-bottom: 5px; font-weight: bold; }
.form-control { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; }
.btn { padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; text-decoration: none; display: inline-block; }
.btn-primary { background: #453227; color: white; }
.btn-success { background: #28a745; color: white; }
.btn-danger { background: #dc3545; color: white; }
.btn-warning { background: #ffc107; color: #333; }
.alert { padding: 15px; border-radius: 4px; margin-bottom: 20px; }
.alert-success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
.alert-danger { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
table { width: 100%; border-collapse: collapse; background: white; }
th, td { padding: 10px; border: 1px solid #ddd; text-align: left; }
th { background: #f8f9fa; }
.action-buttons { display: flex; gap: 5px; }
</style>
</head>
<body>
<div class="admin-header">
<h1><i class="fas fa-user-shield"></i> Админ-панель AETERNA</h1>
<div>
<span><?= htmlspecialchars($_SESSION['user_email'] ?? 'Администратор') ?></span>
<a href="../catalog.php" class="btn btn-primary" style="margin-left: 10px;">В каталог</a>
<a href="../logout.php" class="btn btn-danger" style="margin-left: 10px;">Выйти</a>
</div>
</div>
<div class="admin-tabs">
<a href="?action=dashboard" class="admin-tab <?= $action == 'dashboard' ? 'active' : '' ?>">
<i class="fas fa-tachometer-alt"></i> Дашборд
</a>
<a href="?action=products" class="admin-tab <?= $action == 'products' ? 'active' : '' ?>">
<i class="fas fa-box"></i> Товары
</a>
<a href="?action=categories" class="admin-tab <?= $action == 'categories' ? 'active' : '' ?>">
<i class="fas fa-tags"></i> Категории
</a>
<a href="?action=orders" class="admin-tab <?= $action == 'orders' ? 'active' : '' ?>">
<i class="fas fa-shopping-cart"></i> Заказы
</a>
<a href="?action=users" class="admin-tab <?= $action == 'users' ? 'active' : '' ?>">
<i class="fas fa-users"></i> Пользователи
</a>
</div>
<div class="admin-content">
<?php if ($message): ?>
<div class="alert alert-success">
<i class="fas fa-check-circle"></i> <?= htmlspecialchars(urldecode($message)) ?>
</div>
<?php endif; ?>
<?php if ($error): ?>
<div class="alert alert-danger">
<i class="fas fa-exclamation-circle"></i> <?= htmlspecialchars(urldecode($error)) ?>
</div>
<?php endif; ?>
<?php if ($action == 'dashboard'): ?>
<!-- Дашборд -->
<h2>Статистика</h2>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin: 20px 0;">
<div style="background: white; padding: 20px; border-radius: 5px; text-align: center;">
<h3><?= $stats['total_products'] ?></h3>
<p>Всего товаров</p>
</div>
<div style="background: white; padding: 20px; border-radius: 5px; text-align: center;">
<h3><?= $stats['active_products'] ?></h3>
<p>Активных товаров</p>
</div>
<div style="background: white; padding: 20px; border-radius: 5px; text-align: center;">
<h3><?= $stats['total_orders'] ?></h3>
<p>Заказов</p>
</div>
<div style="background: white; padding: 20px; border-radius: 5px; text-align: center;">
<h3><?= $stats['total_users'] ?></h3>
<p>Пользователей</p>
</div>
</div>
<div style="text-align: center; margin: 40px 0;">
<a href="?action=add_product" class="btn btn-success" style="padding: 15px 30px; font-size: 16px;">
<i class="fas fa-plus"></i> Добавить новый товар
</a>
<a href="?action=add_category" class="btn btn-primary" style="padding: 15px 30px; font-size: 16px;">
<i class="fas fa-plus"></i> Добавить категорию
</a>
</div>
<?php elseif ($action == 'products'): ?>
<!-- Товары -->
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h2>Управление товарами</h2>
<div>
<a href="?action=add_product" class="btn btn-success">
<i class="fas fa-plus"></i> Добавить товар
</a>
<?php if (isset($_GET['show_all'])): ?>
<a href="?action=products" class="btn btn-primary">Только активные</a>
<?php else: ?>
<a href="?action=products&show_all=1" class="btn btn-primary">Показать все</a>
<?php endif; ?>
</div>
</div>
<table>
<thead>
<tr>
<th>ID</th>
<th>Название</th>
<th>Категория</th>
<th>Цена</th>
<th>На складе</th>
<th>Статус</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
<?php foreach ($data as $product): ?>
<tr>
<td><?= $product['product_id'] ?></td>
<td><?= htmlspecialchars($product['name']) ?></td>
<td><?= htmlspecialchars($product['category_name'] ?? 'Без категории') ?></td>
<td><?= number_format($product['price'], 0, '', ' ') ?> ₽</td>
<td><?= $product['stock_quantity'] ?></td>
<td>
<?php if ($product['is_available'] && $product['stock_quantity'] > 0): ?>
<span style="color: green;">✓ Доступен</span>
<?php elseif (!$product['is_available']): ?>
<span style="color: red;">✗ Недоступен</span>
<?php else: ?>
<span style="color: orange;">⚠ Нет на складе</span>
<?php endif; ?>
</td>
<td class="action-buttons">
<a href="?action=edit_product&id=<?= $product['product_id'] ?>" class="btn btn-warning btn-sm">
<i class="fas fa-edit"></i>
</a>
<?php if ($product['is_available']): ?>
<form method="POST" style="display: inline;">
<input type="hidden" name="action" value="edit_product">
<input type="hidden" name="product_id" value="<?= $product['product_id'] ?>">
<input type="hidden" name="is_available" value="0">
<button type="submit" class="btn btn-danger btn-sm" onclick="return confirm('Сделать недоступным?')">
<i class="fas fa-times"></i>
</button>
</form>
<?php else: ?>
<form method="POST" style="display: inline;">
<input type="hidden" name="action" value="edit_product">
<input type="hidden" name="product_id" value="<?= $product['product_id'] ?>">
<input type="hidden" name="is_available" value="1">
<button type="submit" class="btn btn-success btn-sm" onclick="return confirm('Сделать доступным?')">
<i class="fas fa-check"></i>
</button>
</form>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php elseif ($action == 'categories'): ?>
<!-- Категории -->
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h2>Управление категориями</h2>
<a href="?action=add_category" class="btn btn-success">
<i class="fas fa-plus"></i> Добавить категорию
</a>
</div>
<table>
<thead>
<tr>
<th>ID</th>
<th>Название</th>
<th>Slug</th>
<th>Родительская</th>
<th>Товаров</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
<?php foreach ($data as $category): ?>
<tr>
<td><?= $category['category_id'] ?></td>
<td><?= htmlspecialchars($category['name']) ?></td>
<td><?= htmlspecialchars($category['slug']) ?></td>
<td><?= htmlspecialchars($category['parent_name'] ?? '—') ?></td>
<td><?= $category['product_count'] ?></td>
<td class="action-buttons">
<!-- Кнопка редактирования -->
<a href="?action=edit_category&id=<?= $category['category_id'] ?>" class="btn btn-warning btn-sm">
<i class="fas fa-edit"></i> Редактировать
</a>
<!-- Кнопка удаления с AJAX -->
<button type="button" class="btn btn-danger btn-sm delete-category-btn"
data-id="<?= $category['category_id'] ?>"
<?= $category['product_count'] > 0 ? 'disabled' : '' ?>>
<i class="fas fa-trash"></i> Удалить
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php elseif (in_array($action, ['add_product', 'edit_product'])): ?>
<!-- Форма добавления/редактирования товара -->
<div class="form-container">
<h2><?= $action == 'add_product' ? 'Добавление товара' : 'Редактирование товара' ?></h2>
<form method="POST" action="index.php" enctype="multipart/form-data">
<input type="hidden" name="action" value="<?= $action == 'edit_product' ? 'edit_product' : 'add_product' ?>">
<?php if (isset($edit_data)): ?>
<input type="hidden" name="product_id" value="<?= $edit_data['product_id'] ?>">
<?php endif; ?>
<div class="form-group">
<label>Название товара *</label>
<input type="text" name="name" class="form-control"
value="<?= htmlspecialchars($edit_data['name'] ?? '') ?>" required>
</div>
<div class="form-group">
<label>Категория *</label>
<select name="category_id" class="form-control" required>
<option value="">Выберите категорию</option>
<?php foreach ($allCategories as $cat): ?>
<option value="<?= $cat['category_id'] ?>"
<?= (isset($edit_data['category_id']) && $edit_data['category_id'] == $cat['category_id']) ? 'selected' : '' ?>>
<?= htmlspecialchars($cat['name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label>Описание</label>
<textarea name="description" class="form-control" rows="4"><?= htmlspecialchars($edit_data['description'] ?? '') ?></textarea>
</div>
<div style="display: flex; gap: 15px;">
<div class="form-group" style="flex: 1;">
<label>Цена *</label>
<input type="number" name="price" class="form-control" min="0" step="0.01"
value="<?= $edit_data['price'] ?? '' ?>" required>
</div>
<div class="form-group" style="flex: 1;">
<label>Старая цена (для скидки)</label>
<input type="number" name="old_price" class="form-control" min="0" step="0.01"
value="<?= $edit_data['old_price'] ?? '' ?>">
</div>
</div>
<div style="display: flex; gap: 15px;">
<div class="form-group" style="flex: 1;">
<label>Артикул (SKU)</label>
<input type="text" name="sku" class="form-control"
value="<?= htmlspecialchars($edit_data['sku'] ?? '') ?>"
placeholder="Оставьте пустым для автогенерации">
</div>
<div class="form-group" style="flex: 1;">
<label>Количество на складе</label>
<input type="number" name="stock_quantity" class="form-control" min="0"
value="<?= $edit_data['stock_quantity'] ?? 0 ?>">
</div>
</div>
<div class="form-group">
<label>URL изображения</label>
<input type="text" name="image_url" class="form-control"
value="<?= htmlspecialchars($edit_data['image_url'] ?? '') ?>"
placeholder="Например: img2/product.jpg">
</div>
<div style="display: flex; gap: 15px;">
<div class="form-group" style="flex: 1;">
<label>Цвет</label>
<input type="text" name="color" class="form-control"
value="<?= htmlspecialchars($edit_data['color'] ?? '') ?>">
</div>
<div class="form-group" style="flex: 1;">
<label>Материал</label>
<input type="text" name="material" class="form-control"
value="<?= htmlspecialchars($edit_data['material'] ?? '') ?>">
</div>
</div>
<div class="form-group">
<label>
<input type="checkbox" name="is_available" value="1"
<?= (!isset($edit_data['is_available']) || $edit_data['is_available']) ? 'checked' : '' ?>>
Товар доступен
</label>
</div>
<div class="form-group">
<label>
<input type="checkbox" name="is_featured" value="1"
<?= (isset($edit_data['is_featured']) && $edit_data['is_featured']) ? 'checked' : '' ?>>
Рекомендуемый товар
</label>
</div>
<button type="submit" class="btn btn-success">
<?= $action == 'add_product' ? 'Добавить товар' : 'Сохранить изменения' ?>
</button>
<a href="?action=products" class="btn btn-primary">Отмена</a>
</form>
</div>
<?php elseif (in_array($action, ['add_category', 'edit_category'])): ?>
<!-- Форма добавления/редактирования категории -->
<div class="form-container">
<h2><?= $action == 'add_category' ? 'Добавление категории' : 'Редактирование категории' ?></h2>
<form method="POST" action="index.php" id="categoryForm">
<input type="hidden" name="action" value="<?= $action == 'edit_category' ? 'edit_category' : 'add_category' ?>">
<?php if (isset($edit_data)): ?>
<input type="hidden" name="category_id" value="<?= $edit_data['category_id'] ?>">
<?php endif; ?>
<div class="form-group">
<label>Название категории *</label>
<input type="text" name="name" class="form-control"
value="<?= htmlspecialchars($edit_data['name'] ?? '') ?>" required>
</div>
<div class="form-group">
<label>Родительская категория</label>
<select name="parent_id" class="form-control">
<option value="">Без родительской категории</option>
<?php foreach ($parentCategories as $cat): ?>
<?php if (!isset($edit_data['category_id']) || $cat['category_id'] != $edit_data['category_id']): ?>
<option value="<?= $cat['category_id'] ?>"
<?= (isset($edit_data['parent_id']) && $edit_data['parent_id'] == $cat['category_id']) ? 'selected' : '' ?>>
<?= htmlspecialchars($cat['name']) ?>
</option>
<?php endif; ?>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label>Описание</label>
<textarea name="description" class="form-control" rows="3"><?= htmlspecialchars($edit_data['description'] ?? '') ?></textarea>
</div>
<div class="form-group">
<label>Порядок сортировки</label>
<input type="number" name="sort_order" class="form-control" min="0" max="100"
value="<?= $edit_data['sort_order'] ?? 0 ?>">
</div>
<div class="form-group">
<label>
<input type="checkbox" name="is_active" value="1"
<?= (!isset($edit_data['is_active']) || $edit_data['is_active']) ? 'checked' : '' ?>>
Активна
</label>
</div>
<button type="submit" class="btn btn-primary">
<?= $action == 'add_category' ? 'Добавить категорию' : 'Сохранить изменения' ?>
</button>
<a href="?action=categories" class="btn">Отмена</a>
</form>
</div>
<?php elseif ($action == 'orders'): ?>
<!-- Заказы -->
<h2>Заказы</h2>
<table>
<thead>
<tr>
<th>№ заказа</th>
<th>Клиент</th>
<th>Сумма</th>
<th>Статус</th>
<th>Дата</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
<?php foreach ($data as $order): ?>
<tr>
<td><?= htmlspecialchars($order['order_number']) ?></td>
<td><?= htmlspecialchars($order['customer_name']) ?></td>
<td><?= number_format($order['final_amount'], 0, '', ' ') ?> ₽</td>
<td><?= htmlspecialchars($order['status']) ?></td>
<td><?= date('d.m.Y H:i', strtotime($order['created_at'])) ?></td>
<td>
<a href="?action=order_details&id=<?= $order['order_id'] ?>" class="btn btn-primary btn-sm">
<i class="fas fa-eye"></i>
</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php elseif ($action == 'users'): ?>
<!-- Пользователи -->
<h2>Пользователи</h2>
<table>
<thead>
<tr>
<th>ID</th>
<th>Email</th>
<th>ФИО</th>
<th>Дата регистрации</th>
<th>Статус</th>
</tr>
</thead>
<tbody>
<?php foreach ($data as $user): ?>
<tr>
<td><?= $user['user_id'] ?></td>
<td><?= htmlspecialchars($user['email']) ?></td>
<td><?= htmlspecialchars($user['full_name']) ?></td>
<td><?= date('d.m.Y', strtotime($user['created_at'])) ?></td>
<td>
<?php if ($user['is_active']): ?>
<span style="color: green;">✓ Активен</span>
<?php else: ?>
<span style="color: red;">✗ Неактивен</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<script>
// Удаление категории через AJAX
$('.delete-category-btn').click(function() {
const categoryId = $(this).data('id');
const btn = $(this);
if (confirm('Удалить эту категорию?')) {
$.ajax({
url: 'fix_delete_category.php',
method: 'POST',
data: { category_id: categoryId },
success: function(response) {
const result = JSON.parse(response);
if (result.success) {
alert(result.message);
location.reload();
} else {
alert('Ошибка: ' + result.message);
}
}
});
}
});
// Обработка формы категории
$('#categoryForm').submit(function(e) {
e.preventDefault();
$.ajax({
url: $(this).attr('action'),
method: 'POST',
data: $(this).serialize(),
success: function(response) {
const result = JSON.parse(response);
if (result.success) {
alert(result.message);
window.location.href = 'index.php?action=categories';
} else {
alert('Ошибка: ' + result.message);
}
}
});
});
</script>
</body>
</html>

116
public/api/add_to_cart.php Normal file
View File

@@ -0,0 +1,116 @@
<?php
session_start();
require_once __DIR__ . '/../config/database.php';
if (!isset($_SESSION['isLoggedIn']) || $_SESSION['isLoggedIn'] !== true) {
echo json_encode(['success' => false, 'message' => 'Требуется авторизация']);
exit();
}
if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_POST['product_id'])) {
$product_id = intval($_POST['product_id']);
$quantity = intval($_POST['quantity'] ?? 1);
$user_id = $_SESSION['user_id'] ?? 0;
if ($user_id == 0) {
echo json_encode(['success' => false, 'message' => 'Пользователь не найден']);
exit();
}
$db = Database::getInstance()->getConnection();
try {
// Проверяем наличие товара на складе
$checkStock = $db->prepare("
SELECT stock_quantity, name, price
FROM products
WHERE product_id = ? AND is_available = TRUE
");
$checkStock->execute([$product_id]);
$product = $checkStock->fetch();
if (!$product) {
echo json_encode(['success' => false, 'message' => 'Товар не найден']);
exit();
}
if ($product['stock_quantity'] < $quantity) {
echo json_encode(['success' => false, 'message' => 'Недостаточно товара на складе']);
exit();
}
// Проверяем, есть ли товар уже в корзине пользователя
$checkCart = $db->prepare("
SELECT cart_id, quantity
FROM cart
WHERE user_id = ? AND product_id = ?
");
$checkCart->execute([$user_id, $product_id]);
$cartItem = $checkCart->fetch();
if ($cartItem) {
// Обновляем количество
$newQuantity = $cartItem['quantity'] + $quantity;
// Проверяем общее количество
if ($newQuantity > $product['stock_quantity']) {
echo json_encode(['success' => false, 'message' => 'Превышено доступное количество']);
exit();
}
$updateStmt = $db->prepare("
UPDATE cart
SET quantity = ?, updated_at = CURRENT_TIMESTAMP
WHERE cart_id = ?
");
$updateStmt->execute([$newQuantity, $cartItem['cart_id']]);
} else {
// Добавляем новый товар
$insertStmt = $db->prepare("
INSERT INTO cart (user_id, product_id, quantity)
VALUES (?, ?, ?)
");
$insertStmt->execute([$user_id, $product_id, $quantity]);
}
// Обновляем сессию
if (!isset($_SESSION['cart'])) {
$_SESSION['cart'] = [];
}
if (isset($_SESSION['cart'][$product_id])) {
$_SESSION['cart'][$product_id]['quantity'] += $quantity;
} else {
$_SESSION['cart'][$product_id] = [
'quantity' => $quantity,
'name' => $product['name'],
'price' => $product['price'],
'added_at' => time()
];
}
// Получаем общее количество товаров в корзине
$cartCountStmt = $db->prepare("
SELECT SUM(quantity) as total
FROM cart
WHERE user_id = ?
");
$cartCountStmt->execute([$user_id]);
$cart_count = $cartCountStmt->fetchColumn() ?: 0;
echo json_encode([
'success' => true,
'cart_count' => $cart_count,
'message' => 'Товар добавлен в корзину'
]);
} catch (PDOException $e) {
echo json_encode([
'success' => false,
'message' => 'Ошибка базы данных: ' . $e->getMessage()
]);
}
} else {
echo json_encode(['success' => false, 'message' => 'Неверный запрос']);
}
?>

71
public/api/auth.php Normal file
View File

@@ -0,0 +1,71 @@
<?php
// login_handler.php
session_start();
require_once __DIR__ . '/../config/database.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$email = $_POST['email'] ?? '';
$password = $_POST['password'] ?? '';
if (empty($email) || empty($password)) {
echo json_encode(['success' => false, 'message' => 'Заполните все поля']);
exit();
}
$db = Database::getInstance()->getConnection();
try {
// Проверяем пользователя в базе данных
$stmt = $db->prepare("
SELECT user_id, email, password_hash, full_name, phone, city, is_admin, is_active
FROM users
WHERE email = ?
");
$stmt->execute([$email]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$user) {
echo json_encode(['success' => false, 'message' => 'Пользователь не найден']);
exit();
}
if (!$user['is_active']) {
echo json_encode(['success' => false, 'message' => 'Аккаунт заблокирован']);
exit();
}
// Проверяем пароль
if (empty($user['password_hash'])) {
echo json_encode(['success' => false, 'message' => 'Ошибка: пароль не найден в базе данных']);
exit();
}
if (!password_verify($password, $user['password_hash'])) {
echo json_encode(['success' => false, 'message' => 'Неверный пароль']);
exit();
}
// Сохраняем в сессию
$_SESSION['user_id'] = $user['user_id'];
$_SESSION['user_email'] = $user['email'];
$_SESSION['full_name'] = $user['full_name'];
$_SESSION['user_phone'] = $user['phone'] ?? '';
$_SESSION['user_city'] = $user['city'] ?? '';
$_SESSION['isLoggedIn'] = true;
$_SESSION['isAdmin'] = (bool)$user['is_admin'];
$_SESSION['login_time'] = time();
// Обновляем время последнего входа
$updateStmt = $db->prepare("UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE user_id = ?");
$updateStmt->execute([$user['user_id']]);
echo json_encode(['success' => true, 'redirect' => 'catalog.php']);
} catch (PDOException $e) {
echo json_encode(['success' => false, 'message' => 'Ошибка базы данных']);
}
} else {
echo json_encode(['success' => false, 'message' => 'Неверный запрос']);
}
?>

134
public/api/cart.php Normal file
View File

@@ -0,0 +1,134 @@
<?php
/**
* API для работы с корзиной
* Эндпоинты: add, update, remove, get, count
*/
session_start();
require_once __DIR__ . '/../config/database.php';
header('Content-Type: application/json; charset=utf-8');
// Проверка авторизации
if (!isset($_SESSION['isLoggedIn']) || $_SESSION['isLoggedIn'] !== true) {
echo json_encode(['success' => false, 'message' => 'Требуется авторизация']);
exit();
}
$userId = $_SESSION['user_id'] ?? 0;
$action = $_GET['action'] ?? $_POST['action'] ?? '';
$db = Database::getInstance()->getConnection();
try {
switch ($action) {
case 'add':
$productId = (int)($_POST['product_id'] ?? 0);
$quantity = (int)($_POST['quantity'] ?? 1);
if ($productId <= 0) {
echo json_encode(['success' => false, 'message' => 'Неверный ID товара']);
exit();
}
// Проверяем существование товара
$checkProduct = $db->prepare("SELECT product_id, stock_quantity FROM products WHERE product_id = ? AND is_available = TRUE");
$checkProduct->execute([$productId]);
$product = $checkProduct->fetch();
if (!$product) {
echo json_encode(['success' => false, 'message' => 'Товар не найден']);
exit();
}
// Проверяем, есть ли товар уже в корзине
$checkCart = $db->prepare("SELECT cart_id, quantity FROM cart WHERE user_id = ? AND product_id = ?");
$checkCart->execute([$userId, $productId]);
$cartItem = $checkCart->fetch();
if ($cartItem) {
// Обновляем количество
$newQuantity = $cartItem['quantity'] + $quantity;
$stmt = $db->prepare("UPDATE cart SET quantity = ?, updated_at = CURRENT_TIMESTAMP WHERE cart_id = ?");
$stmt->execute([$newQuantity, $cartItem['cart_id']]);
} else {
// Добавляем новый товар
$stmt = $db->prepare("INSERT INTO cart (user_id, product_id, quantity) VALUES (?, ?, ?)");
$stmt->execute([$userId, $productId, $quantity]);
}
echo json_encode(['success' => true, 'message' => 'Товар добавлен в корзину']);
break;
case 'update':
$productId = (int)($_POST['product_id'] ?? 0);
$quantity = (int)($_POST['quantity'] ?? 1);
if ($quantity <= 0) {
// Удаляем товар если количество 0
$stmt = $db->prepare("DELETE FROM cart WHERE user_id = ? AND product_id = ?");
$stmt->execute([$userId, $productId]);
} else {
$stmt = $db->prepare("UPDATE cart SET quantity = ?, updated_at = CURRENT_TIMESTAMP WHERE user_id = ? AND product_id = ?");
$stmt->execute([$quantity, $userId, $productId]);
}
echo json_encode(['success' => true, 'message' => 'Корзина обновлена']);
break;
case 'remove':
$productId = (int)($_POST['product_id'] ?? 0);
$stmt = $db->prepare("DELETE FROM cart WHERE user_id = ? AND product_id = ?");
$stmt->execute([$userId, $productId]);
echo json_encode(['success' => true, 'message' => 'Товар удален из корзины']);
break;
case 'get':
$stmt = $db->prepare("
SELECT c.cart_id, c.product_id, c.quantity, p.name, p.price, p.image_url, p.stock_quantity
FROM cart c
JOIN products p ON c.product_id = p.product_id
WHERE c.user_id = ? AND p.is_available = TRUE
ORDER BY c.created_at DESC
");
$stmt->execute([$userId]);
$items = $stmt->fetchAll();
$total = 0;
foreach ($items as &$item) {
$item['subtotal'] = $item['price'] * $item['quantity'];
$total += $item['subtotal'];
}
echo json_encode([
'success' => true,
'items' => $items,
'total' => $total,
'count' => array_sum(array_column($items, 'quantity'))
]);
break;
case 'count':
$stmt = $db->prepare("SELECT COALESCE(SUM(quantity), 0) FROM cart WHERE user_id = ?");
$stmt->execute([$userId]);
$count = $stmt->fetchColumn();
echo json_encode(['success' => true, 'count' => (int)$count]);
break;
case 'clear':
$stmt = $db->prepare("DELETE FROM cart WHERE user_id = ?");
$stmt->execute([$userId]);
echo json_encode(['success' => true, 'message' => 'Корзина очищена']);
break;
default:
echo json_encode(['success' => false, 'message' => 'Неизвестное действие']);
}
} catch (PDOException $e) {
echo json_encode(['success' => false, 'message' => 'Ошибка базы данных: ' . $e->getMessage()]);
}

62
public/api/get_cart.php Normal file
View File

@@ -0,0 +1,62 @@
<?php
// get_cart.php
session_start();
require_once __DIR__ . '/../config/database.php';
if (!isset($_SESSION['isLoggedIn']) || $_SESSION['isLoggedIn'] !== true) {
echo json_encode(['success' => false, 'message' => 'Требуется авторизация']);
exit();
}
$user_id = $_SESSION['user_id'] ?? 0;
if ($user_id == 0) {
echo json_encode(['success' => false, 'message' => 'Пользователь не найден']);
exit();
}
$db = Database::getInstance()->getConnection();
try {
// Получаем корзину из БД
$stmt = $db->prepare("
SELECT
c.cart_id,
c.product_id,
c.quantity,
p.name,
p.price,
p.image_url,
p.stock_quantity
FROM cart c
JOIN products p ON c.product_id = p.product_id
WHERE c.user_id = ? AND p.is_available = TRUE
ORDER BY c.created_at DESC
");
$stmt->execute([$user_id]);
$cart_items = $stmt->fetchAll();
// Обновляем сессию
$_SESSION['cart'] = [];
foreach ($cart_items as $item) {
$_SESSION['cart'][$item['product_id']] = [
'quantity' => $item['quantity'],
'name' => $item['name'],
'price' => $item['price'],
'added_at' => time()
];
}
echo json_encode([
'success' => true,
'cart_items' => $cart_items,
'total_items' => count($cart_items)
]);
} catch (PDOException $e) {
echo json_encode([
'success' => false,
'message' => 'Ошибка базы данных: ' . $e->getMessage()
]);
}
?>

View File

@@ -0,0 +1,22 @@
<?php
// get_cart_count.php
session_start();
require_once __DIR__ . '/../config/database.php';
if (!isset($_SESSION['isLoggedIn']) || $_SESSION['isLoggedIn'] !== true) {
echo json_encode(['success' => false, 'cart_count' => 0]);
exit();
}
$user_id = $_SESSION['user_id'] ?? 0;
$db = Database::getInstance()->getConnection();
try {
$stmt = $db->prepare("SELECT SUM(quantity) as total FROM cart WHERE user_id = ?");
$stmt->execute([$user_id]);
$cart_count = $stmt->fetchColumn() ?: 0;
echo json_encode(['success' => true, 'cart_count' => $cart_count]);
} catch (PDOException $e) {
echo json_encode(['success' => false, 'cart_count' => 0]);
}

View File

@@ -0,0 +1,33 @@
<?php
session_start();
require_once __DIR__ . '/../config/database.php';
// Проверяем авторизацию администратора
if (!isset($_SESSION['isAdmin']) || $_SESSION['isAdmin'] !== true) {
echo json_encode(['success' => false, 'message' => 'Доступ запрещен']);
exit();
}
if (!isset($_GET['id'])) {
echo json_encode(['success' => false, 'message' => 'ID не указан']);
exit();
}
try {
$db = Database::getInstance()->getConnection();
$product_id = $_GET['id'];
$stmt = $db->prepare("SELECT * FROM products WHERE product_id = ?");
$stmt->execute([$product_id]);
$product = $stmt->fetch();
if ($product) {
echo json_encode($product);
} else {
echo json_encode(['success' => false, 'message' => 'Товар не найден']);
}
} catch (PDOException $e) {
echo json_encode(['success' => false, 'message' => 'Ошибка базы данных: ' . $e->getMessage()]);
}
?>

View File

@@ -0,0 +1,134 @@
<?php
// process_order.php
session_start();
require_once __DIR__ . '/../config/database.php';
if (!isset($_SESSION['isLoggedIn']) || $_SESSION['isLoggedIn'] !== true) {
header('Location: login.php?error=auth_required');
exit();
}
$user_id = $_SESSION['user_id'] ?? 0;
if ($user_id == 0) {
header('Location: login.php?error=user_not_found');
exit();
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$db = Database::getInstance()->getConnection();
try {
$db->beginTransaction();
// Получаем данные из формы
$customer_name = $_POST['full_name'] ?? '';
$customer_email = $_POST['email'] ?? '';
$customer_phone = $_POST['phone'] ?? '';
$delivery_address = $_POST['address'] ?? '';
$region = $_POST['region'] ?? '';
$payment_method = $_POST['payment'] ?? 'card';
$delivery_method = $_POST['delivery'] ?? 'courier';
$notes = $_POST['notes'] ?? '';
$discount_amount = floatval($_POST['discount'] ?? 0);
$delivery_cost = floatval($_POST['delivery_price'] ?? 2000);
// Генерируем номер заказа
$order_number = 'ORD-' . date('Ymd-His') . '-' . rand(1000, 9999);
// Получаем корзину пользователя
$cartStmt = $db->prepare("
SELECT
c.product_id,
c.quantity,
p.name,
p.price,
p.stock_quantity
FROM cart c
JOIN products p ON c.product_id = p.product_id
WHERE c.user_id = ?
");
$cartStmt->execute([$user_id]);
$cart_items = $cartStmt->fetchAll();
if (empty($cart_items)) {
throw new Exception('Корзина пуста');
}
// Рассчитываем итоги
$total_amount = 0;
foreach ($cart_items as $item) {
$total_amount += $item['price'] * $item['quantity'];
}
$final_amount = $total_amount - $discount_amount + $delivery_cost;
// Создаем заказ
$orderStmt = $db->prepare("
INSERT INTO orders (
user_id, order_number, total_amount, discount_amount,
delivery_cost, final_amount, status, payment_method,
delivery_method, delivery_address, customer_name,
customer_email, customer_phone, notes
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
RETURNING order_id
");
$orderStmt->execute([
$user_id, $order_number, $total_amount, $discount_amount,
$delivery_cost, $final_amount, 'pending', $payment_method,
$delivery_method, $delivery_address, $customer_name,
$customer_email, $customer_phone, $notes
]);
$order_id = $orderStmt->fetchColumn();
// Добавляем товары в заказ и обновляем остатки
foreach ($cart_items as $item) {
// Добавляем в order_items
$itemStmt = $db->prepare("
INSERT INTO order_items (
order_id, product_id, product_name,
quantity, unit_price, total_price
) VALUES (?, ?, ?, ?, ?, ?)
");
$item_total = $item['price'] * $item['quantity'];
$itemStmt->execute([
$order_id, $item['product_id'], $item['name'],
$item['quantity'], $item['price'], $item_total
]);
// Обновляем остатки на складе
$updateStmt = $db->prepare("
UPDATE products
SET stock_quantity = stock_quantity - ?,
updated_at = CURRENT_TIMESTAMP
WHERE product_id = ?
");
$updateStmt->execute([$item['quantity'], $item['product_id']]);
}
// Очищаем корзину
$clearCartStmt = $db->prepare("DELETE FROM cart WHERE user_id = ?");
$clearCartStmt->execute([$user_id]);
// Очищаем сессию
unset($_SESSION['cart']);
$db->commit();
// Перенаправляем на страницу успеха
header('Location: order_success.php?id=' . $order_id);
exit();
} catch (Exception $e) {
$db->rollBack();
header('Location: checkout.php?error=' . urlencode($e->getMessage()));
exit();
}
} else {
header('Location: checkout.php');
exit();
}
?>

View File

@@ -0,0 +1,175 @@
<?php
// register_handler.php
session_start();
require_once __DIR__ . '/../config/database.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$errors = [];
// Получаем данные из формы
$full_name = trim($_POST['fio'] ?? '');
$city = trim($_POST['city'] ?? '');
$email = trim($_POST['email'] ?? '');
$phone = trim($_POST['phone'] ?? '');
$password = $_POST['password'] ?? '';
$confirm_password = $_POST['confirm-password'] ?? '';
// Валидация данных
if (empty($full_name) || strlen($full_name) < 3) {
$errors[] = 'ФИО должно содержать минимум 3 символа';
}
if (empty($city) || strlen($city) < 2) {
$errors[] = 'Введите корректное название города';
}
if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors[] = 'Введите корректный email адрес';
}
if (empty($phone) || !preg_match('/^(\+7|8)[\s-]?\(?\d{3}\)?[\s-]?\d{3}[\s-]?\d{2}[\s-]?\d{2}$/', $phone)) {
$errors[] = 'Введите корректный номер телефона';
}
if (empty($password) || strlen($password) < 6) {
$errors[] = 'Пароль должен содержать минимум 6 символов';
}
if ($password !== $confirm_password) {
$errors[] = 'Пароли не совпадают';
}
// Проверка согласия с условиями
if (!isset($_POST['privacy']) || $_POST['privacy'] !== 'on') {
$errors[] = 'Необходимо согласие с условиями обработки персональных данных';
}
// Если есть ошибки, возвращаем на форму
if (!empty($errors)) {
$_SESSION['registration_errors'] = $errors;
$_SESSION['old_data'] = [
'fio' => $full_name,
'city' => $city,
'email' => $email,
'phone' => $phone
];
header('Location: ../register.php');
exit();
}
// Подключаемся к базе данных
$db = Database::getInstance()->getConnection();
try {
// Проверяем, существует ли пользователь с таким email
$checkStmt = $db->prepare("SELECT user_id FROM users WHERE email = ?");
$checkStmt->execute([$email]);
if ($checkStmt->fetch()) {
$_SESSION['registration_errors'] = ['Пользователь с таким email уже существует'];
$_SESSION['old_data'] = [
'fio' => $full_name,
'city' => $city,
'email' => $email,
'phone' => $phone
];
header('Location: ../register.php');
exit();
}
// Хэшируем пароль
$password_hash = password_hash($password, PASSWORD_DEFAULT);
// Определяем, является ли пользователь администратором
$is_admin = false;
$admin_emails = ['admin@aeterna.ru', 'administrator@aeterna.ru', 'aeterna@mail.ru'];
if (in_array(strtolower($email), $admin_emails)) {
$is_admin = true;
}
// Используем CAST для правильной передачи boolean в PostgreSQL
$stmt = $db->prepare("
INSERT INTO users (email, password_hash, full_name, phone, city, is_admin, is_active)
VALUES (?, ?, ?, ?, ?, CAST(? AS boolean), TRUE)
RETURNING user_id
");
$stmt->execute([
$email,
$password_hash,
$full_name,
$phone,
$city,
$is_admin ? 'true' : 'false' // Строковые значения true/false для CAST
]);
$user_id = $stmt->fetchColumn();
if (!$user_id) {
throw new Exception('Ошибка при создании пользователя: user_id не получен');
}
// Проверяем, что пользователь действительно создался
$verifyStmt = $db->prepare("SELECT user_id, email, password_hash FROM users WHERE user_id = ?");
$verifyStmt->execute([$user_id]);
$verifyUser = $verifyStmt->fetch(PDO::FETCH_ASSOC);
if (!$verifyUser) {
throw new Exception('Ошибка: пользователь не найден после создания');
}
// Проверяем, что пароль сохранился правильно
if (empty($verifyUser['password_hash'])) {
throw new Exception('Ошибка: пароль не сохранен');
}
// Автоматически авторизуем пользователя
$_SESSION['user_id'] = $user_id;
$_SESSION['user_email'] = $email;
$_SESSION['full_name'] = $full_name;
$_SESSION['user_phone'] = $phone;
$_SESSION['user_city'] = $city;
$_SESSION['isLoggedIn'] = true;
$_SESSION['isAdmin'] = (bool)$is_admin;
$_SESSION['login_time'] = time();
// Обновляем время последнего входа
$updateStmt = $db->prepare("UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE user_id = ?");
$updateStmt->execute([$user_id]);
// Перенаправляем на главную или каталог
$_SESSION['registration_success'] = 'Регистрация прошла успешно! ' .
($is_admin ? 'Вы зарегистрированы как администратор.' : 'Добро пожаловать в AETERNA!');
header('Location: ../catalog.php');
exit();
} catch (PDOException $e) {
// Логируем полную ошибку для отладки
error_log("Registration DB Error: " . $e->getMessage());
error_log("SQL State: " . $e->getCode());
error_log("Email: " . $email);
$_SESSION['registration_errors'] = ['Ошибка базы данных: ' . $e->getMessage()];
$_SESSION['old_data'] = [
'fio' => $full_name,
'city' => $city,
'email' => $email,
'phone' => $phone
];
header('Location: ../register.php');
exit();
} catch (Exception $e) {
error_log("Registration Error: " . $e->getMessage());
$_SESSION['registration_errors'] = [$e->getMessage()];
header('Location: ../register.php');
exit();
}
} else {
// Если запрос не POST, перенаправляем на форму регистрации
header('Location: register.php');
exit();
}
?>

View File

@@ -0,0 +1,31 @@
<?php
session_start();
require_once __DIR__ . '/../config/database.php';
if (!isset($_SESSION['isLoggedIn']) || $_SESSION['isLoggedIn'] !== true) {
echo json_encode(['success' => false, 'message' => 'Требуется авторизация']);
exit();
}
if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_POST['product_id'])) {
$product_id = intval($_POST['product_id']);
$quantity = intval($_POST['quantity'] ?? 1);
$user_id = $_SESSION['user_id'] ?? 0;
$db = Database::getInstance()->getConnection();
try {
$stmt = $db->prepare("
UPDATE cart
SET quantity = ?, updated_at = CURRENT_TIMESTAMP
WHERE user_id = ? AND product_id = ?
");
$stmt->execute([$quantity, $user_id, $product_id]);
echo json_encode(['success' => true]);
} catch (PDOException $e) {
echo json_encode(['success' => false, 'message' => 'Ошибка базы данных']);
}
}
?>

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

BIN
public/assets/img/1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

BIN
public/assets/img/100.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
public/assets/img/11.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

BIN
public/assets/img/111.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
public/assets/img/11_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

BIN
public/assets/img/1_1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
public/assets/img/1_2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

BIN
public/assets/img/1_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

BIN
public/assets/img/2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
public/assets/img/22.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
public/assets/img/25.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
public/assets/img/2_2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
public/assets/img/2_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

BIN
public/assets/img/3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

BIN
public/assets/img/3_3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

BIN
public/assets/img/3_3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 414 KiB

BIN
public/assets/img/4.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
public/assets/img/44.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
public/assets/img/444 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

BIN
public/assets/img/444.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
public/assets/img/444.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 KiB

BIN
public/assets/img/4_1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

BIN
public/assets/img/5.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

BIN
public/assets/img/5_5.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
public/assets/img/5_5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

BIN
public/assets/img/6.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

BIN
public/assets/img/6_6.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
public/assets/img/6_6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 KiB

BIN
public/assets/img/7.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

BIN
public/assets/img/77.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 KiB

BIN
public/assets/img/777.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

BIN
public/assets/img/777.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 555 KiB

BIN
public/assets/img/7_7.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
public/assets/img/7_7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 479 KiB

BIN
public/assets/img/8.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

BIN
public/assets/img/88.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

BIN
public/assets/img/888.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
public/assets/img/888.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

BIN
public/assets/img/8_8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 430 KiB

BIN
public/assets/img/9.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

BIN
public/assets/img/99.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

BIN
public/assets/img/99.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
public/assets/img/99_1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

BIN
public/assets/img/99_2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

BIN
public/assets/img/99_3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 497 KiB

BIN
public/assets/img/9_9.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
public/assets/img/9_9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 759 KiB

BIN
public/assets/img/black.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 654 KiB

BIN
public/assets/img/brown.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 739 KiB

BIN
public/assets/img/chair.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

BIN
public/assets/img/gray.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
public/assets/img/gray1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

BIN
public/assets/img/gray2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 655 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

View File

@@ -0,0 +1,62 @@
.error-message {
color: #ff0000;
font-size: 12px;
margin-top: 5px;
display: none;
}
.form__input.error {
border-color: #ff0000;
}
.form__group {
position: relative;
margin-bottom: 15px;
}
/* Стили для сообщений внизу страницы */
.page-messages {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 1000;
width: 90%;
max-width: 500px;
}
.message {
padding: 15px;
margin: 10px 0;
border-radius: 5px;
text-align: center;
font-weight: bold;
display: none;
}
.message.error {
background-color: #ffebee;
color: #c62828;
border: 1px solid #ffcdd2;
}
.message.success {
background-color: #e8f5e9;
color: #453227;
border: 1px solid #c8e6c9;
}
.message.warning {
background-color: #fff3e0;
color: #ef6c00;
border: 1px solid #ffe0b2;
}
.privacy-error {
color: #ff0000;
font-size: 12px;
margin-top: 5px;
display: none;
text-align: center;
}

View File

@@ -0,0 +1,346 @@
// script.js
$(document).ready(function() {
// Инициализация корзины
let cart = {
items: [
{ id: 1, name: 'Кресло OPPORTUNITY', price: 16999, quantity: 1 },
{ id: 2, name: 'Кресло GOLDEN', price: 19999, quantity: 1 },
{ id: 3, name: 'Светильник POLET', price: 7999, quantity: 1 }
],
delivery: 2000,
discount: 0
};
// Функция обновления общей суммы
function updateTotal() {
let productsTotal = 0;
let totalCount = 0;
// Пересчитываем товары
$('.products__item').each(function() {
const $item = $(this);
const price = parseInt($item.data('price'));
const quantity = parseInt($item.find('.products__qty-value').text());
productsTotal += price * quantity;
totalCount += quantity;
});
// Обновляем отображение
$('.products-total').text(productsTotal + ' ₽');
$('.summary-count').text(totalCount);
$('.total-count').text(totalCount + ' шт.');
$('.cart-count').text(totalCount);
// Обновляем итоговую сумму
const finalTotal = productsTotal + cart.delivery - cart.discount;
$('.final-total').text(finalTotal + ' ₽');
}
// Функция валидации email
function validateEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
// Функция валидации имени (ФИО)
function validateFullName(name) {
// Проверяем, что имя содержит только буквы, пробелы, дефисы и апострофы
const nameRegex = /^[a-zA-Zа-яА-ЯёЁ\s\-']+$/;
// Проверяем, что имя состоит минимум из 2 слов
const words = name.trim().split(/\s+/);
return nameRegex.test(name) && words.length >= 2;
}
// Функция валидации телефона
function validatePhone(phone) {
// Российский формат телефона: +7XXXXXXXXXX
const phoneRegex = /^\+7\d{10}$/;
return phoneRegex.test(phone);
}
// Функция отображения сообщения
function showMessage(messageId, duration = 5000) {
// Скрываем все сообщения
$('.message').hide();
// Показываем нужное сообщение
$(messageId).fadeIn(300);
// Автоматически скрываем через указанное время
if (duration > 0) {
setTimeout(() => {
$(messageId).fadeOut(300);
}, duration);
}
}
// Функция показа ошибки приватности
function showPrivacyError(show) {
if (show) {
$('#privacy-error').show();
} else {
$('#privacy-error').hide();
}
}
// Функция отображения ошибки конкретного поля
function showFieldError(fieldId, message) {
// Убираем старые ошибки
$(fieldId).removeClass('error-input');
$(fieldId + '-error').remove();
if (message) {
$(fieldId).addClass('error-input');
$(fieldId).after('<div class="field-error" id="' + fieldId.replace('#', '') + '-error">' + message + '</div>');
}
}
// Валидация поля при потере фокуса с указанием конкретной ошибки
$('#fullname').on('blur', function() {
const value = $(this).val().trim();
if (value) {
if (!validateFullName(value)) {
showFieldError('#fullname', 'ФИО должно содержать только буквы и состоять минимум из 2 слов');
} else {
showFieldError('#fullname', '');
}
} else {
showFieldError('#fullname', '');
}
});
$('#email').on('blur', function() {
const value = $(this).val().trim();
if (value) {
if (!validateEmail(value)) {
showFieldError('#email', 'Введите корректный email адрес (например: example@mail.ru)');
} else {
showFieldError('#email', '');
}
} else {
showFieldError('#email', '');
}
});
$('#phone').on('blur', function() {
const value = $(this).val().trim();
if (value) {
if (!validatePhone(value)) {
showFieldError('#phone', 'Введите номер в формате +7XXXXXXXXXX (10 цифр после +7)');
} else {
showFieldError('#phone', '');
}
} else {
showFieldError('#phone', '');
}
});
// Валидация обязательных полей
$('#region').on('blur', function() {
const value = $(this).val().trim();
if (!value) {
showFieldError('#region', 'Укажите регион доставки');
} else {
showFieldError('#region', '');
}
});
$('#address').on('blur', function() {
const value = $(this).val().trim();
if (!value) {
showFieldError('#address', 'Укажите адрес доставки (улица, дом, квартира)');
} else {
showFieldError('#address', '');
}
});
// Очистка ошибки при начале ввода
$('.form__input').on('input', function() {
const fieldId = '#' + $(this).attr('id');
showFieldError(fieldId, '');
});
// Обработчик увеличения количества
$('.products__qty-btn.plus').click(function() {
const $qtyValue = $(this).siblings('.products__qty-value');
let quantity = parseInt($qtyValue.text());
$qtyValue.text(quantity + 1);
updateTotal();
});
// Обработчик уменьшения количества
$('.products__qty-btn.minus').click(function() {
const $qtyValue = $(this).siblings('.products__qty-value');
let quantity = parseInt($qtyValue.text());
if (quantity > 1) {
$qtyValue.text(quantity - 1);
updateTotal();
}
});
// Обработчик удаления товара
$('.remove-from-cart').click(function() {
const $productItem = $(this).closest('.products__item');
$productItem.fadeOut(300, function() {
$(this).remove();
updateTotal();
// Показываем сообщение, если корзина пуста
if ($('.products__item').length === 0) {
$('.products__list').html('<div class="empty-cart">Корзина пуста</div>');
}
});
});
// Обработчик применения промокода
$('.promo__btn').click(function() {
const promoCode = $('.promo__input').val().toUpperCase();
if (promoCode === 'SALE10') {
cart.discount = Math.round(parseInt($('.products-total').text()) * 0.1);
$('.discount-total').text(cart.discount + ' ₽');
showMessage('#form-error', 3000);
$('#form-error').text('Промокод применен! Скидка 10%').removeClass('error').addClass('success');
} else if (promoCode === 'FREE') {
cart.delivery = 0;
$('.delivery-price').text('0 ₽');
showMessage('#form-error', 3000);
$('#form-error').text('Промокод применен! Бесплатная доставка').removeClass('error').addClass('success');
} else if (promoCode) {
showMessage('#form-error', 3000);
$('#form-error').text('Промокод недействителен').removeClass('success').addClass('error');
}
updateTotal();
});
// Обработчик выбора доставки
$('input[name="delivery"]').change(function() {
if ($(this).val() === 'pickup') {
cart.delivery = 0;
$('.delivery-price').text('0 ₽');
} else {
cart.delivery = 2000;
$('.delivery-price').text('2000 ₽');
}
updateTotal();
});
// Функция проверки всех полей формы
function validateForm() {
let isValid = true;
let errorMessages = [];
// Очищаем все старые ошибки
$('.field-error').remove();
$('.form__input').removeClass('error-input');
// Проверка обязательных полей
const requiredFields = [
{
id: '#fullname',
value: $('#fullname').val().trim(),
validator: validateFullName,
required: true,
message: 'ФИО должно содержать только буквы и состоять минимум из 2 слов'
},
{
id: '#phone',
value: $('#phone').val().trim(),
validator: validatePhone,
required: true,
message: 'Введите корректный номер телефона в формате +7XXXXXXXXXX'
},
{
id: '#email',
value: $('#email').val().trim(),
validator: validateEmail,
required: true,
message: 'Введите корректный email адрес'
},
{
id: '#region',
value: $('#region').val().trim(),
validator: (val) => val.length > 0,
required: true,
message: 'Поле "Регион" обязательно для заполнения'
},
{
id: '#address',
value: $('#address').val().trim(),
validator: (val) => val.length > 0,
required: true,
message: 'Поле "Адрес" обязательно для заполнения'
}
];
// Проверяем каждое поле
requiredFields.forEach(field => {
if (field.required && (!field.value || !field.validator(field.value))) {
isValid = false;
errorMessages.push(field.message);
showFieldError(field.id, field.message);
}
});
// Проверка согласия на обработку данных
if (!$('#privacy-checkbox').is(':checked')) {
isValid = false;
showPrivacyError(true);
errorMessages.push('Необходимо согласие на обработку персональных данных');
} else {
showPrivacyError(false);
}
// Показываем общее сообщение, если есть ошибки
if (!isValid && errorMessages.length > 0) {
showMessage('#form-error', 5000);
$('#form-error').text('Исправьте следующие ошибки: ' + errorMessages.join('; ')).removeClass('success').addClass('error');
// Прокручиваем к первой ошибке
$('html, body').animate({
scrollTop: $('.error-input').first().offset().top - 100
}, 500);
}
return isValid;
}
// Обработчик оформления заказа
$('#submit-order').click(function() {
// Проверка валидации всех полей
if (!validateForm()) {
return;
}
// Симуляция отправки заказа
$(this).prop('disabled', true).text('ОБРАБОТКА...');
setTimeout(() => {
showMessage('#order-success', 5000);
$(this).prop('disabled', false).text('ОФОРМИТЬ ЗАКАЗ');
// Здесь можно добавить редирект на страницу благодарности
// window.location.href = 'спасибо.html';
}, 2000);
});
// Маска для телефона
$('#phone').on('input', function() {
let phone = $(this).val().replace(/\D/g, '');
if (phone.length > 0) {
if (phone[0] !== '7' && phone[0] !== '8') {
phone = '7' + phone;
}
phone = '+7' + phone.substring(1, 11);
$(this).val(phone);
}
});
// Инициализация при загрузке
updateTotal();
});

384
public/assets/js/profile.js Normal file
View File

@@ -0,0 +1,384 @@
$(document).ready(function() {
// Функции для отображения сообщений
function showMessage(type, text) {
const messageId = type + 'Message';
const $message = $('#' + messageId);
$message.text(text).fadeIn(300);
setTimeout(() => {
$message.fadeOut(300);
}, 5000);
}
// Проверка, является ли email администратора
function isAdminEmail(email) {
const adminEmails = ['admin@aeterna.ru', 'administrator@aeterna.ru', 'aeterna@mail.ru'];
return adminEmails.includes(email.toLowerCase());
}
// Валидация ФИО (без цифр)
function validateFIO(fio) {
const words = fio.trim().split(/\s+/);
// Проверяем, что минимум 2 слова и нет цифр
const hasNoDigits = !/\d/.test(fio);
return words.length >= 2 && words.every(word => word.length >= 2) && hasNoDigits;
}
// Валидация города
function validateCity(city) {
return city.trim().length >= 2 && /^[а-яА-ЯёЁ\s-]+$/.test(city);
}
// Валидация email
function validateEmail(email) {
const localPart = email.split('@')[0];
// Проверка на кириллические символы перед @
if (/[а-яА-ЯёЁ]/.test(localPart)) {
showError('email', 'В тексте перед знаком "@" не должно быть кириллических символов');
return false;
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
showError('email', 'Введите корректный email адрес');
return false;
}
hideError('email');
return true;
}
// Валидация телефона
function validatePhone(phone) {
const phoneRegex = /^(\+7|8)[\s-]?\(?\d{3}\)?[\s-]?\d{3}[\s-]?\d{2}[\s-]?\d{2}$/;
return phoneRegex.test(phone.replace(/\s/g, ''));
}
// Валидация пароля
function validatePassword(password) {
return password.length >= 6;
}
// Показать/скрыть ошибку
function showError(fieldId, message) {
$('#' + fieldId).addClass('error');
$('#' + fieldId + '-error').text(message).show();
}
function hideError(fieldId) {
$('#' + fieldId).removeClass('error');
$('#' + fieldId + '-error').hide();
}
// Валидация формы
function validateForm() {
let isValid = true;
// Валидация ФИО
const fio = $('#fio').val();
if (!validateFIO(fio)) {
if (/\d/.test(fio)) {
showError('fio', 'ФИО не должно содержать цифры');
} else {
showError('fio', 'ФИО должно содержать минимум 2 слова (каждое от 2 символов)');
}
isValid = false;
} else {
hideError('fio');
}
// Валидация города
const city = $('#city').val();
if (!validateCity(city)) {
showError('city', 'Укажите корректное название города (только русские буквы)');
isValid = false;
} else {
hideError('city');
}
// Валидация email
const email = $('#email').val();
if (!validateEmail(email)) {
showError('email', 'Введите корректный email адрес');
isValid = false;
} else {
hideError('email');
}
// Валидация телефона
const phone = $('#phone').val();
if (!validatePhone(phone)) {
showError('phone', 'Введите номер в формате: +7(912)999-12-23');
isValid = false;
} else {
hideError('phone');
}
// Валидация пароля
const password = $('#password').val();
if (!validatePassword(password)) {
showError('password', 'Пароль должен содержать минимум 6 символов');
isValid = false;
} else {
hideError('password');
}
// Проверка совпадения паролей
const confirmPassword = $('#confirm-password').val();
if (password !== confirmPassword) {
showError('confirm-password', 'Пароли не совпадают');
isValid = false;
} else {
hideError('confirm-password');
}
// Проверка согласия с условиями
if (!$('#privacy').is(':checked')) {
$('#privacy-error').show();
isValid = false;
} else {
$('#privacy-error').hide();
}
return isValid;
}
// Реальная валидация при вводе
$('input').on('blur', function() {
const fieldId = $(this).attr('id');
const value = $(this).val();
switch(fieldId) {
case 'fio':
if (!validateFIO(value)) {
if (/\d/.test(value)) {
showError(fieldId, 'ФИО не должно содержать цифры');
} else {
showError(fieldId, 'ФИО должно содержать минимум 2 слова');
}
} else {
hideError(fieldId);
}
break;
case 'city':
if (!validateCity(value)) {
showError(fieldId, 'Укажите корректное название города');
} else {
hideError(fieldId);
}
break;
case 'email':
if (!validateEmail(value)) {
showError(fieldId, 'Введите корректный email адрес');
} else {
hideError(fieldId);
}
break;
case 'phone':
if (!validatePhone(value)) {
showError(fieldId, 'Введите номер в формате: +7(912)999-12-23');
} else {
hideError(fieldId);
}
break;
case 'password':
if (!validatePassword(value)) {
showError(fieldId, 'Пароль должен содержать минимум 6 символов');
} else {
hideError(fieldId);
}
break;
case 'confirm-password':
const password = $('#password').val();
if (value !== password) {
showError(fieldId, 'Пароли не совпадают');
} else {
hideError(fieldId);
}
break;
}
});
// Обработка отправки формы
$('#registrationForm').on('submit', function(e) {
e.preventDefault();
if (validateForm()) {
const email = $('#email').val();
const isAdmin = isAdminEmail(email);
// Сохраняем данные пользователя в localStorage
const userData = {
email: email,
fio: $('#fio').val(),
phone: $('#phone').val(),
isAdmin: isAdmin,
registered: new Date().toISOString()
};
localStorage.setItem('userData', JSON.stringify(userData));
localStorage.setItem('isLoggedIn', 'true');
localStorage.setItem('isAdmin', isAdmin.toString());
// Эмуляция успешной регистрации
showMessage('success', 'Регистрация прошла успешно! ' +
(isAdmin ? 'Вы зарегистрированы как администратор.' : 'Добро пожаловать в AETERNA!'));
setTimeout(() => {
// Перенаправление на главную страницу
window.location.href = 'cite_mebel.php';
}, 2000);
} else {
showMessage('error', 'Пожалуйста, исправьте ошибки в форме');
}
});
// Плавная прокрутка к якорям
$('a[href^="#"]').on('click', function(event) {
var target = $(this.getAttribute('href'));
if (target.length) {
event.preventDefault();
$('html, body').stop().animate({
scrollTop: target.offset().top
}, 1000);
}
});
// Переключение между регистрацией и входом
$('.login-btn').on('click', function() {
showMessage('warning', 'Переход к форме входа...');
setTimeout(() => {
window.location.href = 'вход.php';
}, 1000);
});
// Обработка ссылки "Сменить пароль"
$('.password-link').on('click', function(e) {
e.preventDefault();
showMessage('warning', 'Функция смены пароля будет доступна после регистрации');
});
// Маска для телефона
$('#phone').on('input', function() {
let value = $(this).val().replace(/\D/g, '');
if (value.startsWith('7') || value.startsWith('8')) {
value = value.substring(1);
}
if (value.length > 0) {
value = '+7(' + value;
if (value.length > 6) value = value.substring(0, 6) + ')' + value.substring(6);
if (value.length > 10) value = value.substring(0, 10) + '-' + value.substring(10);
if (value.length > 13) value = value.substring(0, 13) + '-' + value.substring(13);
if (value.length > 16) value = value.substring(0, 16);
}
$(this).val(value);
});
// Запрет ввода цифр в поле ФИО
$('#fio').on('input', function() {
let value = $(this).val();
// Удаляем цифры из значения
value = value.replace(/\d/g, '');
$(this).val(value);
});
});
// Для входа (файл вход.html)
$(document).ready(function() {
// Проверяем, если пользователь уже вошел
if (localStorage.getItem('isLoggedIn') === 'true') {
const userData = JSON.parse(localStorage.getItem('userData') || '{}');
if (userData.email) {
$('#login-email').val(userData.email);
}
}
// Функция проверки пароля администратора
function checkAdminPassword(email, password) {
// Административные аккаунты
const adminAccounts = {
'admin@aeterna.ru': 'admin123',
'administrator@aeterna.ru': 'admin123',
'aeterna@mail.ru': 'admin123'
};
return adminAccounts[email.toLowerCase()] === password;
}
// Валидация формы входа
$('#loginForm').on('submit', function(e) {
e.preventDefault();
let isValid = true;
const email = $('#login-email').val();
const password = $('#login-password').val();
// Валидация email
if (!isValidEmail(email)) {
$('#email-error').show();
isValid = false;
} else {
$('#email-error').hide();
}
// Валидация пароля
if (password.length < 6) {
$('#password-error').show();
isValid = false;
} else {
$('#password-error').hide();
}
if (isValid) {
// Здесь обычно отправка данных на сервер
showMessage('success', 'Вы успешно вошли в систему!');
// Перенаправление на главную страницу через 1.5 секунды
setTimeout(function() {
window.location.href = 'cite_mebel.php';
}, 1500);
}
});
// Функция показа сообщений
function showMessage(type, text) {
const messageId = type + 'Message';
const $message = $('#' + messageId);
$message.text(text).show();
setTimeout(function() {
$message.fadeOut();
}, 3000);
}
// Скрываем сообщения об ошибках при фокусе на полях
$('input').on('focus', function() {
$(this).next('.error-message').hide();
});
// Обработка чекбокса "Запомнить меня"
$('#remember').on('change', function() {
if ($(this).is(':checked')) {
const email = $('#login-email').val();
if (email) {
localStorage.setItem('rememberedEmail', email);
}
} else {
localStorage.removeItem('rememberedEmail');
}
});
// Автозаполнение email, если пользователь выбрал "Запомнить меня"
const rememberedEmail = localStorage.getItem('rememberedEmail');
if (rememberedEmail) {
$('#login-email').val(rememberedEmail);
$('#remember').prop('checked', true);
}
// Обработка ссылки "Забыли пароль?"
$('.forgot-password').on('click', function(e) {
e.preventDefault();
showMessage('info', 'Для восстановления пароля обратитесь к администратору');
});
});

View File

@@ -0,0 +1,142 @@
.error-message {
color: #ff0000;
font-size: 12px;
margin-top: 5px;
display: none;
}
.form__input.error {
border-color: #ff0000;
}
.form__group {
position: relative;
margin-bottom: 15px;
}
/* Стили для сообщений внизу страницы */
.page-messages {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 1000;
width: 90%;
max-width: 500px;
}
.message {
padding: 15px;
margin: 10px 0;
border-radius: 5px;
text-align: center;
font-weight: bold;
display: none;
}
.message.error {
background-color: #ffebee;
color: #c62828;
border: 1px solid #ffcdd2;
}
.message.success {
background-color: #e8f5e9;
color: #453227;
border: 1px solid #c8e6c9;
}
.message.warning {
background-color: #fff3e0;
color: #ef6c00;
border: 1px solid #ffe0b2;
}
.privacy-error {
color: #ff0000;
font-size: 12px;
margin-top: 5px;
display: none;
text-align: center;
}
/* Дополнительные стили для формы регистрации */
.input-group {
position: relative;
margin-bottom: 20px;
}
.profile-form input.error {
border-color: #ff0000;
background-color: #fff5f5;
}
.privacy-checkbox {
margin: 20px 0;
text-align: center;
}
.privacy-checkbox label {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
cursor: pointer;
font-size: 14px;
}
.privacy-checkbox input[type="checkbox"] {
margin: 0;
}
/* Исправление отступов для страницы регистрации */
.profile-page-main {
padding: 40px 0;
min-height: calc(100vh - 200px);
}
/* Убедимся, что контейнер не перекрывает шапку и футер */
.profile-container {
margin: 0 auto;
position: relative;
z-index: 1;
}
.input-hint {
font-size: 12px;
color: #666;
margin-top: 5px;
}
/* Стили для страницы входа */
.form-options {
display: flex;
justify-content: space-between;
align-items: center;
margin: 20px 0;
}
.remember-me {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: #453227;
}
.remember-me input[type="checkbox"] {
width: 16px;
height: 16px;
cursor: pointer;
}
.forgot-password {
font-size: 14px;
color: #453227;
text-decoration: underline;
}
.forgot-password:hover {
color: #617365;
text-decoration: none;
}

View File

@@ -0,0 +1,86 @@
// ===================================
// === ПЕРЕМЕННЫЕ И МИКСИНЫ AETERNA ===
// ===================================
@color-primary: #617365;
@color-secondary: #D1D1D1;
@color-accent: #453227;
@color-text-dark: #333;
@color-text-light: #fff;
@color-button: @color-accent;
@color-beige: #A2A09A;
@font-logo: 'Anek Kannada', sans-serif;
@font-main: 'Anonymous Pro', monospace;
@font-heading: 'Playfair Display', serif;
@shadow-light: 0 5px 15px rgba(0, 0, 0, 0.2);
@shadow-dark: 2px 2px 4px rgba(0, 0, 0, 0.3);
.flex-center(@gap: 0) {
display: flex;
align-items: center;
justify-content: center;
gap: @gap;
}
.flex-between() {
display: flex;
align-items: center;
justify-content: space-between;
}
.flex-column() {
display: flex;
flex-direction: column;
}
.icon-base(@size: 18px, @hover-scale: 1.1) {
cursor: pointer;
transition: all 0.3s ease;
font-size: @size;
&:hover {
transform: scale(@hover-scale);
}
}
.image-overlay() {
position: absolute;
inset: 0;
.flex-center(15px);
flex-direction: column;
text-align: center;
background-color: rgba(0, 0, 0, 0.4);
padding: 20px;
color: @color-text-light;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.7);
transition: all 0.3s ease;
}
.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: 1000;
margin-top: 5px;
display: none;
}
.input-base() {
width: 100%;
padding: 12px 15px;
border: 1px solid #ccc;
background-color: #fff;
font-family: @font-main;
font-size: 14px;
outline: none;
transition: border-color 0.3s ease;
&:focus {
border-color: @color-primary;
}
}

File diff suppressed because it is too large Load Diff

1343
public/catalog.php Normal file

File diff suppressed because it is too large Load Diff

114
public/check_auth.js Normal file
View File

@@ -0,0 +1,114 @@
// check_auth.js
$(document).ready(function() {
// Проверка авторизации при загрузке страницы
checkAuthStatus();
// Обработка формы входа
$('#loginForm').on('submit', function(e) {
e.preventDefault();
const email = $('#login-email').val();
const password = $('#login-password').val();
const remember = $('#remember').is(':checked');
$.ajax({
url: 'login_handler.php',
method: 'POST',
data: {
email: email,
password: password
},
success: function(response) {
try {
const result = JSON.parse(response);
if (result.success) {
// Сохраняем в localStorage если выбрано "Запомнить меня"
if (remember) {
localStorage.setItem('rememberedEmail', email);
} else {
localStorage.removeItem('rememberedEmail');
}
// Перенаправляем
window.location.href = result.redirect || 'catalog.php';
} else {
showMessage('error', result.message || 'Ошибка авторизации');
}
} catch(e) {
showMessage('error', 'Ошибка обработки ответа');
}
},
error: function() {
showMessage('error', 'Ошибка сервера');
}
});
});
// Проверка статуса авторизации
function checkAuthStatus() {
$.ajax({
url: 'check_auth_status.php',
method: 'GET',
success: function(response) {
try {
const result = JSON.parse(response);
if (result.loggedIn) {
updateUserProfile(result.user);
}
} catch(e) {
console.error('Ошибка проверки авторизации', e);
}
}
});
}
// Обновление профиля пользователя
function updateUserProfile(user) {
// Обновляем шапку, если есть элементы для профиля
if ($('#userEmail').length) {
$('#userEmail').text(user.email);
}
if ($('#userName').length) {
$('#userName').text(user.full_name);
}
}
// Показать сообщение
function showMessage(type, text) {
const $message = $('#' + type + 'Message');
if ($message.length) {
$message.text(text).fadeIn();
setTimeout(() => $message.fadeOut(), 5000);
} else {
alert(text);
}
}
// Проверка авторизации для ссылок
function checkAuth(redirectUrl) {
$.ajax({
url: 'check_auth_status.php',
method: 'GET',
success: function(response) {
try {
const result = JSON.parse(response);
if (result.loggedIn) {
window.location.href = redirectUrl;
} else {
// Показываем модальное окно или перенаправляем на вход
showLoginModal(redirectUrl);
}
} catch(e) {
showLoginModal(redirectUrl);
}
}
});
return false;
}
// Показать модальное окно входа
function showLoginModal(redirectUrl) {
// Можно реализовать модальное окно или перенаправить на страницу входа
window.location.href = 'вход.php?redirect=' + encodeURIComponent(redirectUrl);
}
});

505
public/checkout.php Normal file
View File

@@ -0,0 +1,505 @@
<?php
// checkout.php
session_start();
require_once __DIR__ . '/../config/database.php';
// Проверка авторизации
if (!isset($_SESSION['isLoggedIn']) || $_SESSION['isLoggedIn'] !== true) {
header('Location: login.php?error=auth_required');
exit();
}
$user_id = $_SESSION['user_id'] ?? 0;
$db = Database::getInstance()->getConnection();
// Получаем корзину пользователя
$cart_items = [];
$total_amount = 0;
$total_quantity = 0;
try {
$stmt = $db->prepare("
SELECT
c.cart_id,
c.product_id,
c.quantity,
p.name,
p.price,
p.image_url,
p.stock_quantity
FROM cart c
JOIN products p ON c.product_id = p.product_id
WHERE c.user_id = ? AND p.is_available = TRUE
ORDER BY c.created_at DESC
");
$stmt->execute([$user_id]);
$cart_items = $stmt->fetchAll();
// Рассчитываем общую сумму
foreach ($cart_items as $item) {
$total_amount += $item['price'] * $item['quantity'];
$total_quantity += $item['quantity'];
}
} catch (PDOException $e) {
$error = "Ошибка загрузки корзины: " . $e->getMessage();
}
?>
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AETERNA - Оформление заказа</title>
<link rel="stylesheet/less" type="text/css" href="style_for_cite.less">
<script src="https://cdn.jsdelivr.net/npm/less"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<style>
.empty-cart {
text-align: center;
padding: 40px;
font-size: 18px;
color: #666;
}
.cart-error {
background: #f8d7da;
color: #721c24;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
.continue-shopping {
text-align: center;
margin-top: 30px;
}
.continue-shopping .btn {
background: #453227;
color: white;
padding: 12px 30px;
border-radius: 4px;
text-decoration: none;
display: inline-block;
}
</style>
</head>
<body>
<?php include 'header_common.php'; ?>
<main class="container">
<div class="main__content">
<section class="products">
<div class="breadcrumbs">
<a href="cite_mebel.php">Главная</a> • <span class="current-page">Корзина</span>
</div>
<h2 class="products__title">Товары в корзине</h2>
<?php if (empty($cart_items)): ?>
<div class="empty-cart">
<i class="fas fa-shopping-cart" style="font-size: 48px; color: #ccc; margin-bottom: 20px;"></i>
<p>Ваша корзина пуста</p>
<div class="continue-shopping">
<a href="/catalog.php" class="btn">Продолжить покупки</a>
</div>
</div>
<?php else: ?>
<div class="products__list" id="cartItems">
<?php foreach ($cart_items as $item): ?>
<div class="products__item" data-product-id="<?= $item['product_id'] ?>" data-price="<?= $item['price'] ?>">
<div class="products__image">
<img src="<?= htmlspecialchars($item['image_url'] ?? 'img1/default.jpg') ?>"
alt="<?= htmlspecialchars($item['name']) ?>"
class="product-img">
</div>
<div class="products__details">
<div class="products__name"><?= htmlspecialchars($item['name']) ?></div>
<div class="products__price"><?= number_format($item['price'], 0, '', ' ') ?> ₽</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>
<?php endif; ?>
</section>
<?php if (!empty($cart_items)): ?>
<section class="order">
<form method="POST" action="api/process_order.php" id="orderForm">
<div class="order__header">
<h2 class="order__title">Оформление заказа</h2>
<div class="order__total">Товары, <span class="total-count"><?= $total_quantity ?></span></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="delivery" class="form__radio-input" value="courier" checked>
<span class="form__custom-radio"></span>
Курьерская доставка
</label>
<label class="form__radio-label">
<input type="radio" name="delivery" class="form__radio-input" value="pickup">
<span class="form__custom-radio"></span>
Самовывоз
</label>
</div>
<div class="form__group">
<label for="fullname" class="form__label">КОНТАКТЫ</label>
<input type="text" id="fullname" name="full_name" class="form__input"
placeholder="ФИО..."
value="<?= htmlspecialchars($_SESSION['full_name'] ?? '') ?>"
required>
</div>
<div class="form__group form__row">
<div style="position: relative; width: 48%;">
<input type="tel" id="phone" name="phone" class="form__input form__input--half"
placeholder="Телефон..."
value="<?= htmlspecialchars($_SESSION['user_phone'] ?? '') ?>"
required>
</div>
<div style="position: relative; width: 48%;">
<input type="email" id="email" name="email" class="form__input form__input--half"
placeholder="E-mail..."
value="<?= htmlspecialchars($_SESSION['user_email'] ?? '') ?>"
required>
</div>
</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" class="form__radio-input" value="card" checked>
<span class="form__custom-radio"></span>
Банковская карта
</label>
<label class="form__radio-label">
<input type="radio" name="payment" class="form__radio-input" value="cash">
<span class="form__custom-radio"></span>
Наличные
</label>
</div>
<div class="form__group form__row">
<div style="position: relative; width: 48%;">
<input type="text" id="region" name="region" class="form__input form__input--half"
placeholder="Регион..." required>
</div>
<div style="position: relative; width: 48%;">
<input type="text" id="index" name="postal_code" class="form__input form__input--half"
placeholder="Индекс (необязательно)...">
</div>
</div>
<div class="form__group">
<input type="text" id="address" name="address" class="form__input"
placeholder="Улица, дом, квартира..." required>
</div>
</div>
<div class="divider"></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>
<div class="summary">
<div class="summary__item">
<span>Товары, <span class="summary-count"><?= $total_quantity ?></span> шт.</span>
<span class="products-total"><?= number_format($total_amount, 0, '', ' ') ?> ₽</span>
</div>
<div class="summary__item">
<span>Скидка</span>
<span class="discount-total">0 ₽</span>
<input type="hidden" name="discount" value="0">
</div>
<div class="summary__item">
<span>Доставка</span>
<span class="delivery-price">2000 ₽</span>
<input type="hidden" name="delivery_price" value="2000">
</div>
<div class="summary__item total">
<span>ИТОГО:</span>
<span class="final-total"><?= number_format($total_amount + 2000, 0, '', ' ') ?> ₽</span>
</div>
</div>
<button type="submit" class="order-btn" id="submit-order">ОФОРМИТЬ ЗАКАЗ</button>
<label class="privacy">
<input type="checkbox" id="privacy-checkbox" required>
Даю согласие на обработку персональных данных
</label>
<div class="privacy-error" id="privacy-error" style="display: none; color: #dc3545; font-size: 14px; margin-top: 10px;">
Необходимо согласие на обработку персональных данных
</div>
<div class="services">
<h3 class="services__title">УСЛУГИ</h3>
<div class="services__item">
<span>Доставка</span>
<span>2000 ₽</span>
</div>
<div class="services__item">
<span>Сборка</span>
<span>1000 ₽</span>
</div>
</div>
</form>
</section>
<?php endif; ?>
</div>
</main>
<div class="page-messages">
<div class="message error" id="form-error" style="display: none;"></div>
<div class="message success" id="order-success" style="display: none;"></div>
</div>
<?php
// Если файла footer.php нет, используем встроенный футер
if (file_exists('footer.php')) {
include 'footer.php';
} else {
// Встроенный футер
?>
<footer class="footer" id="footer">
<div class="container footer__content">
<div class="footer__col footer--logo">
<div class="logo">AETERNA</div>
</div>
<div class="footer__col">
<h5>ПОКУПАТЕЛЮ</h5>
<ul>
<li><a href="/catalog.php">Каталог</a></li>
<li><a href="services.php">Услуги</a></li>
</ul>
</div>
<div class="footer__col">
<h5>ПОМОЩЬ</h5>
<ul>
<li><a href="delivery.php">Доставка и оплата</a></li>
<li><a href="warranty.php">Гарантия и возврат</a></li>
<li><a href="cite_mebel.php#faq">Ответы на вопросы</a></li>
<li><a href="#footer">Контакты</a></li>
</ul>
</div>
<div class="footer__col">
<h5>КОНТАКТЫ</h5>
<p>aeterna@mail.ru</p>
<p>+7(912)999-12-23</p>
<div class="social-icons">
<span class="icon"><i class="fab fa-telegram"></i></span>
<span class="icon"><i class="fab fa-instagram"></i></span>
<span class="icon"><i class="fab fa-vk"></i></span>
</div>
</div>
<div class="footer__col">
<h5>ПРИНИМАЕМ К ОПЛАТЕ</h5>
<div class="payment-icons">
<span class="pay-icon"><i class="fab fa-cc-visa"></i></span>
<span class="pay-icon"><i class="fab fa-cc-mastercard"></i></span>
</div>
</div>
</div>
<div class="copyright">
<p>© 2025 AETERNA. Все права защищены.</p>
</div>
</footer>
<?php
}
?>
<script>
$(document).ready(function() {
// Обновление количества товаров
$('.products__qty-btn').on('click', function(e) {
e.preventDefault();
e.stopPropagation();
const $btn = $(this);
const productId = $btn.data('id');
const isPlus = $btn.hasClass('plus');
const $qtyValue = $btn.siblings('.products__qty-value');
let quantity = parseInt($qtyValue.text());
if (isPlus) {
quantity++;
} else if (quantity > 1) {
quantity--;
} else {
return; // Не уменьшаем ниже 1
}
// Временно обновляем UI
$qtyValue.text(quantity);
$.ajax({
url: 'api/cart.php',
method: 'POST',
data: {
action: 'update',
product_id: productId,
quantity: quantity
},
dataType: 'json',
success: function(result) {
if (result.success) {
updateTotals();
} else {
// Откатываем изменения при ошибке
$qtyValue.text(isPlus ? quantity - 1 : quantity + 1);
alert('Ошибка: ' + result.message);
}
},
error: function(xhr, status, error) {
console.error('AJAX ошибка:', status, error);
// Откатываем изменения
$qtyValue.text(isPlus ? quantity - 1 : quantity + 1);
alert('Ошибка сервера');
}
});
});
// Удаление товара
$('.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: 'api/cart.php',
method: 'POST',
data: {
action: 'remove',
product_id: productId
},
dataType: 'json',
success: function(result) {
if (result.success) {
$item.fadeOut(300, function() {
$(this).remove();
updateTotals();
// Если корзина пуста, показываем сообщение
if ($('#cartItems .products__item').length === 0) {
$('#cartItems').html('<div class="empty-cart"><i class="fas fa-shopping-cart" style="font-size: 48px; color: #ccc; margin-bottom: 20px;"></i><p>Ваша корзина пуста</p></div>');
$('.order').hide();
}
});
} else {
alert('Ошибка: ' + result.message);
}
},
error: function(xhr, status, error) {
console.error('AJAX ошибка:', status, error);
alert('Ошибка сервера');
}
});
});
// Обновление итогов
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);
$('.total-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') + ' ₽');
showMessage('Промокод применен! Скидка 10%', 'success');
updateTotals();
} else if (promoCode === 'FREE') {
$('input[name="delivery_price"]').val(0);
$('.delivery-price').text('0 ₽');
showMessage('Промокод применен! Бесплатная доставка', 'success');
updateTotals();
} else if (promoCode) {
showMessage('Промокод недействителен', 'error');
}
});
// Обработка формы заказа
$('#orderForm').submit(function(e) {
e.preventDefault();
if (!$('#privacy-checkbox').is(':checked')) {
$('#privacy-error').show();
return;
}
$('#privacy-error').hide();
$('#submit-order').prop('disabled', true).text('ОБРАБОТКА...');
$.ajax({
url: 'api/process_order.php',
method: 'POST',
data: $(this).serialize(),
success: function(response) {
try {
const result = JSON.parse(response);
if (result.success) {
window.location.href = 'order_success.php?id=' + result.order_id;
} else {
showMessage('Ошибка: ' + result.message, 'error');
$('#submit-order').prop('disabled', false).text('ОФОРМИТЬ ЗАКАЗ');
}
} catch(e) {
showMessage('Ошибка обработки заказа', 'error');
$('#submit-order').prop('disabled', false).text('ОФОРМИТЬ ЗАКАЗ');
}
}
});
});
function showMessage(message, type) {
const $msg = type === 'success' ? $('#order-success') : $('#form-error');
$msg.text(message).fadeIn(300);
setTimeout(() => $msg.fadeOut(300), 5000);
}
});
</script>
</body>
</html>

114
public/config/check_auth.js Normal file
View File

@@ -0,0 +1,114 @@
// check_auth.js
$(document).ready(function() {
// Проверка авторизации при загрузке страницы
checkAuthStatus();
// Обработка формы входа
$('#loginForm').on('submit', function(e) {
e.preventDefault();
const email = $('#login-email').val();
const password = $('#login-password').val();
const remember = $('#remember').is(':checked');
$.ajax({
url: 'login_handler.php',
method: 'POST',
data: {
email: email,
password: password
},
success: function(response) {
try {
const result = JSON.parse(response);
if (result.success) {
// Сохраняем в localStorage если выбрано "Запомнить меня"
if (remember) {
localStorage.setItem('rememberedEmail', email);
} else {
localStorage.removeItem('rememberedEmail');
}
// Перенаправляем
window.location.href = result.redirect || 'catalog.php';
} else {
showMessage('error', result.message || 'Ошибка авторизации');
}
} catch(e) {
showMessage('error', 'Ошибка обработки ответа');
}
},
error: function() {
showMessage('error', 'Ошибка сервера');
}
});
});
// Проверка статуса авторизации
function checkAuthStatus() {
$.ajax({
url: 'check_auth_status.php',
method: 'GET',
success: function(response) {
try {
const result = JSON.parse(response);
if (result.loggedIn) {
updateUserProfile(result.user);
}
} catch(e) {
console.error('Ошибка проверки авторизации', e);
}
}
});
}
// Обновление профиля пользователя
function updateUserProfile(user) {
// Обновляем шапку, если есть элементы для профиля
if ($('#userEmail').length) {
$('#userEmail').text(user.email);
}
if ($('#userName').length) {
$('#userName').text(user.full_name);
}
}
// Показать сообщение
function showMessage(type, text) {
const $message = $('#' + type + 'Message');
if ($message.length) {
$message.text(text).fadeIn();
setTimeout(() => $message.fadeOut(), 5000);
} else {
alert(text);
}
}
// Проверка авторизации для ссылок
function checkAuth(redirectUrl) {
$.ajax({
url: 'check_auth_status.php',
method: 'GET',
success: function(response) {
try {
const result = JSON.parse(response);
if (result.loggedIn) {
window.location.href = redirectUrl;
} else {
// Показываем модальное окно или перенаправляем на вход
showLoginModal(redirectUrl);
}
} catch(e) {
showLoginModal(redirectUrl);
}
}
});
return false;
}
// Показать модальное окно входа
function showLoginModal(redirectUrl) {
// Можно реализовать модальное окно или перенаправить на страницу входа
window.location.href = 'вход.php?redirect=' + encodeURIComponent(redirectUrl);
}
});

View File

@@ -0,0 +1,32 @@
<?php
// config/database.php
class Database {
private static $instance = null;
private $connection;
private function __construct() {
try {
$this->connection = new PDO(
"pgsql:host=185.130.224.177;port=5481;dbname=postgres",
"admin",
"38feaad2840ccfda0e71243a6faaecfd"
);
$this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$this->connection->exec("SET NAMES 'utf8'");
} catch(PDOException $e) {
die("Ошибка подключения: " . $e->getMessage());
}
}
public static function getInstance() {
if (self::$instance == null) {
self::$instance = new Database();
}
return self::$instance;
}
public function getConnection() {
return $this->connection;
}
}
?>

174
public/delivery.php Normal file
View File

@@ -0,0 +1,174 @@
<?php
// В начале файла
require_once __DIR__ . '/../config/database.php';
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>AETERNA - Доставка и оплата</title>
<link rel="stylesheet/less" type="text/css" href="style_for_cite.less">
<script src="https://cdn.jsdelivr.net/npm/less"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
<script src="check_auth.js"></script>
<style>
/* Стили для блокировки доступа */
.link-disabled {
cursor: not-allowed !important;
opacity: 0.7 !important;
pointer-events: none !important;
}
.auth-required {
position: relative;
}
.auth-required::after {
content: "🔒";
position: absolute;
top: -5px;
right: -10px;
font-size: 10px;
}
</style>
</head>
<body>
<?php include 'header_common.php'; ?>
<main class="catalog-main">
<div class="container">
<div class="breadcrumbs">
<a href="cite_mebel.php">Главная</a> • <span class="current-page">Доставка и оплата</span>
</div>
<div class="delivery-content">
<h1>ДОСТАВКА И ОПЛАТА</h1>
<div class="delivery-section">
<div class="delivery-card">
<div class="delivery-icon">
<i class="fas fa-truck"></i>
</div>
<h3>Курьерская доставка</h3>
<div class="delivery-details">
<div class="detail-item">
<span class="detail-label">Бесплатная доставка:</span>
<span class="detail-value">при заказе от 30 000 ₽</span>
</div>
<div class="detail-item">
<span class="detail-label">В пределах МКАД:</span>
<span class="detail-value">1 500 ₽</span>
</div>
<div class="detail-item">
<span class="detail-label">За МКАД:</span>
<span class="detail-value">1 500 ₽ + 50 ₽/км</span>
</div>
<div class="detail-item">
<span class="detail-label">Время доставки:</span>
<span class="detail-value">с 9:00 до 21:00</span>
</div>
</div>
</div>
<div class="delivery-card">
<div class="delivery-icon">
<i class="fas fa-store"></i>
</div>
<h3>Самовывоз из шоурума</h3>
<div class="delivery-details">
<div class="detail-item">
<span class="detail-label">Адрес:</span>
<span class="detail-value">г. Москва, ул. Дизайнерская, 15</span>
</div>
<div class="detail-item">
<span class="detail-label">Стоимость:</span>
<span class="detail-value">Бесплатно</span>
</div>
<div class="detail-item">
<span class="detail-label">Время получения:</span>
<span class="detail-value">в течение 2 часов после подтверждения</span>
</div>
<div class="detail-item">
<span class="detail-label">Парковка:</span>
<span class="detail-value">Бесплатная для клиентов</span>
</div>
</div>
</div>
<div class="delivery-card">
<div class="delivery-icon">
<i class="fas fa-map-marked-alt"></i>
</div>
<h3>Доставка по России</h3>
<div class="delivery-details">
<div class="detail-item">
<span class="detail-label">Стоимость:</span>
<span class="detail-value">рассчитывается индивидуально</span>
</div>
<div class="detail-item">
<span class="detail-label">Сроки:</span>
<span class="detail-value">от 3 до 14 дней</span>
</div>
<div class="detail-item">
<span class="detail-label">Транспортные компании:</span>
<span class="detail-value">СДЭК, Boxberry, Деловые Линии</span>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<footer class="footer" id="footer">
<div class="container footer__content">
<div class="footer__col footer__col--logo">
<div class="logo">AETERNA</div>
</div>
<div class="footer__col">
<h5>ПОКУПАТЕЛЮ</h5>
<ul>
<li><a href="/catalog.php">Каталог</a></li>
<li><a href="services.php">Услуги</a></li>
</ul>
</div>
<div class="footer__col">
<h5>ПОМОЩЬ</h5>
<ul>
<li><a href="доставка.php">Доставка и оплата</a></li>
<li><a href="гарантия.php">Гарантия и возврат</a></li>
<li><a href="cite_mebel.php#faq">Ответы на вопросы</a></li>
<li><a href="#footer">Контакты</a></li>
</ul>
</div>
<div class="footer__col">
<h5>КОНТАКТЫ</h5>
<p>aeterna@mail.ru</p>
<p>+7(912)999-12-23</p>
<div class="social-icons">
<span class="icon"><i class="fab fa-telegram"></i></span>
<span class="icon"><i class="fab fa-instagram"></i></span>
<span class="icon"><i class="fab fa-vk"></i></span>
</div>
</div>
<div class="footer__col">
<h5>ПРИНИМАЕМ К ОПЛАТЕ</h5>
<div class="payment-icons">
<span class="pay-icon"><i class="fab fa-cc-visa"></i></span>
<span class="pay-icon"><i class="fab fa-cc-mastercard"></i></span>
</div>
</div>
</div>
<div class="copyright">
<p>© 2025 AETERNA. Все права защищены.</p>
</div>
</footer>
</body>
</html>

50
public/footer.php Normal file
View File

@@ -0,0 +1,50 @@
<footer class="footer" id="footer">
<div class="container footer__content">
<div class="footer__col footer--logo">
<div class="logo">AETERNA</div>
</div>
<div class="footer__col">
<h5>ПОКУПАТЕЛЮ</h5>
<ul>
<li><a href="catalog.php">Каталог</a></li>
<li><a href="services.php">Услуги</a></li>
</ul>
</div>
<div class="footer__col">
<h5>ПОМОЩЬ</h5>
<ul>
<li><a href="delivery.php">Доставка и оплата</a></li>
<li><a href="warranty.php">Гарантия и возврат</a></li>
<li><a href="index.php#faq">Ответы на вопросы</a></li>
<li><a href="#footer">Контакты</a></li>
</ul>
</div>
<div class="footer__col">
<h5>КОНТАКТЫ</h5>
<p>aeterna@mail.ru</p>
<p>+7(912)999-12-23</p>
<div class="social-icons">
<span class="icon"><i class="fab fa-telegram"></i></span>
<span class="icon"><i class="fab fa-instagram"></i></span>
<span class="icon"><i class="fab fa-vk"></i></span>
</div>
</div>
<div class="footer__col">
<h5>ПРИНИМАЕМ К ОПЛАТЕ</h5>
<div class="payment-icons">
<span class="pay-icon"><i class="fab fa-cc-visa"></i></span>
<span class="pay-icon"><i class="fab fa-cc-mastercard"></i></span>
</div>
</div>
</div>
<div class="copyright">
<p>© 2025 AETERNA. Все права защищены.</p>
</div>
</footer>
</body>
</html>

169
public/header_common.php Normal file
View File

@@ -0,0 +1,169 @@
<?php
/**
* Единый header для страниц AETERNA (версия для public/)
* Подключение: include 'header_common.php';
*/
// Запускаем сессию если еще не запущена
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// Определяем текущую страницу
$currentPage = basename($_SERVER['PHP_SELF'], '.php');
// Проверяем авторизацию
$isLoggedIn = isset($_SESSION['isLoggedIn']) && $_SESSION['isLoggedIn'] === true;
$isAdmin = isset($_SESSION['isAdmin']) && $_SESSION['isAdmin'] === true;
$userEmail = $_SESSION['user_email'] ?? '';
$fullName = $_SESSION['full_name'] ?? $userEmail;
?>
<header class="header">
<div class="header__top">
<div class="container header__top-content">
<div class="logo"><a href="index.php" style="text-decoration: none; color: inherit;">AETERNA</a></div>
<div class="search-catalog">
<div class="catalog-dropdown">
Все категории <span>&#9660;</span>
<div class="catalog-dropdown__menu">
<ul>
<li><a href="catalog.php">Все товары</a></li>
<li><a href="catalog.php?category=1">Диваны</a></li>
<li><a href="catalog.php?category=2">Кресла</a></li>
<li><a href="catalog.php?category=3">Кровати</a></li>
<li><a href="catalog.php?category=4">Столы</a></li>
<li><a href="catalog.php?category=5">Стулья</a></li>
</ul>
</div>
</div>
<div class="search-box">
<form method="GET" action="catalog.php" style="display: flex; width: 100%;">
<input type="text" name="search" placeholder="Поиск товаров" style="border: none; width: 100%; padding: 10px;">
<button type="submit" style="background: none; border: none; cursor: pointer;">
<span class="search-icon"><i class="fas fa-search"></i></span>
</button>
</form>
</div>
</div>
<div class="header__icons--top">
<?php if ($isLoggedIn): ?>
<!-- Иконка корзины -->
<a href="checkout.php" class="icon cart-icon">
<i class="fas fa-shopping-cart"></i>
<span class="cart-count" id="cartCount">0</span>
</a>
<!-- Блок профиля -->
<div class="user-profile-dropdown">
<div class="user-profile-toggle" id="profileToggle">
<div class="user-avatar"><?= !empty($userEmail) ? strtoupper(substr($userEmail, 0, 1)) : 'U' ?></div>
<div class="user-info">
<div class="user-email"><?= htmlspecialchars($userEmail) ?></div>
<div class="user-status <?= $isAdmin ? 'admin' : 'user' ?>"><?= $isAdmin ? 'Админ' : 'Пользователь' ?></div>
</div>
<i class="fas fa-chevron-down" style="font-size: 12px; color: #666;"></i>
</div>
<div class="user-profile-menu" id="profileMenu">
<div class="user-profile-header">
<div class="user-profile-name"><i class="fas fa-user"></i> <?= htmlspecialchars($fullName) ?></div>
</div>
<ul class="user-profile-links">
<li><a href="profile.php"><i class="fas fa-user-cog"></i> Мой профиль</a></li>
<li><a href="checkout.php"><i class="fas fa-shopping-bag"></i> Мои заказы</a></li>
<?php if ($isAdmin): ?>
<li><a href="admin/index.php"><i class="fas fa-user-shield"></i> Админ-панель</a></li>
<?php endif; ?>
<li><a href="logout.php" class="logout-link"><i class="fas fa-sign-out-alt"></i> Выйти</a></li>
</ul>
</div>
</div>
<?php else: ?>
<a href="login.php" class="icon"><i class="far fa-user"></i></a>
<a href="login.php" style="font-size: 12px; color: #666; margin-left: 5px;">Войти</a>
<?php endif; ?>
</div>
</div>
</div>
<div class="header__bottom">
<div class="container header__bottom-content">
<div class="catalog-menu">
<a href="catalog.php" class="catalog-link <?= $currentPage === 'catalog' ? 'active' : '' ?>">
<span class="catalog-lines">☰</span> Каталог
</a>
</div>
<nav class="nav">
<ul class="nav-list">
<li><a href="index.php" class="<?= $currentPage === 'index' || $currentPage === 'cite_mebel' ? 'active' : '' ?>">Главная</a></li>
<li><a href="services.php" class="<?= $currentPage === 'services' ? 'active' : '' ?>">Услуги</a></li>
<li><a href="delivery.php" class="<?= $currentPage === 'delivery' ? 'active' : '' ?>">Доставка и оплата</a></li>
<li><a href="warranty.php" class="<?= $currentPage === 'warranty' ? 'active' : '' ?>">Гарантия</a></li>
<li><a href="#footer">Контакты</a></li>
</ul>
</nav>
<div class="header-phone">+7(912)999-12-23</div>
</div>
</div>
</header>
<style>
/* Стили для профиля пользователя */
.user-profile-dropdown { position: relative; display: inline-block; }
.user-profile-toggle { display: flex; align-items: center; gap: 10px; cursor: pointer; padding: 8px 12px; border-radius: 4px; transition: all 0.3s ease; }
.user-profile-toggle:hover { background-color: rgba(0, 0, 0, 0.05); }
.user-avatar { width: 36px; height: 36px; border-radius: 50%; background: linear-gradient(135deg, #617365 0%, #453227 100%); color: white; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 16px; }
.user-info { display: flex; flex-direction: column; }
.user-email { font-size: 12px; color: #666; max-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.user-status { font-size: 10px; padding: 2px 8px; border-radius: 12px; text-transform: uppercase; font-weight: bold; text-align: center; margin-top: 2px; }
.user-status.admin { background-color: #617365; color: white; }
.user-status.user { background-color: #28a745; color: white; }
.user-profile-menu { display: none; position: absolute; top: 100%; right: 0; background: white; min-width: 280px; border-radius: 8px; box-shadow: 0 5px 20px rgba(0,0,0,0.15); z-index: 1000; padding-top: 10px; border: 1px solid #e0e0e0; overflow: visible; }
.user-profile-menu::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 10px; background: transparent; }
.user-profile-dropdown:hover .user-profile-menu { display: block; }
.user-profile-header { padding: 15px; background: #f8f9fa; border-bottom: 1px solid #e0e0e0; }
.user-profile-name { font-weight: bold; margin-bottom: 5px; color: #333; display: flex; align-items: center; gap: 8px; }
.user-profile-links { list-style: none; padding: 10px 0; margin: 0; }
.user-profile-links a { display: flex; align-items: center; gap: 10px; padding: 10px 15px; color: #333; text-decoration: none; transition: all 0.3s ease; border-left: 3px solid transparent; }
.user-profile-links a:hover { background-color: #f5f5f5; border-left-color: #453227; color: #453227; }
.logout-link { color: #dc3545 !important; }
.logout-link:hover { background-color: #ffe6e6 !important; border-left-color: #dc3545 !important; }
.cart-icon { position: relative; margin-right: 15px; }
.cart-count { position: absolute; top: -8px; right: -8px; background: #dc3545; color: white; border-radius: 50%; width: 18px; height: 18px; font-size: 11px; display: flex; align-items: center; justify-content: center; }
</style>
<script>
$(document).ready(function() {
// Профиль пользователя - открытие/закрытие
$('#profileToggle').click(function(e) {
e.stopPropagation();
$('#profileMenu').toggle();
});
$(document).click(function(e) {
if (!$(e.target).closest('.user-profile-dropdown').length) {
$('#profileMenu').hide();
}
});
// Обновление счетчика корзины
<?php if ($isLoggedIn): ?>
$.ajax({
url: 'api/cart.php?action=count',
method: 'GET',
success: function(response) {
try {
const result = JSON.parse(response);
if (result.success) {
$('#cartCount').text(result.count);
}
} catch(e) {}
}
});
<?php endif; ?>
});
</script>

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

BIN
public/img/1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

BIN
public/img/100.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
public/img/11.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Some files were not shown because too many files have changed in this diff Show More