Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -21,7 +21,7 @@ DATA_FILE = 'data.json'
|
|
| 21 |
|
| 22 |
SYNC_FILES = [DATA_FILE]
|
| 23 |
|
| 24 |
-
REPO_ID = "Kgshop/prazdnik"
|
| 25 |
HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
|
| 26 |
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
|
| 27 |
|
|
@@ -30,7 +30,7 @@ STORE_ADDRESS = "Рынок Джунхай , 6 проход , 49А контей
|
|
| 30 |
CURRENCY_CODE = 'KGS'
|
| 31 |
CURRENCY_NAME = 'Кыргызский сом'
|
| 32 |
|
| 33 |
-
WHATSAPP_NUMBER = "996702888188"
|
| 34 |
|
| 35 |
DOWNLOAD_RETRIES = 3
|
| 36 |
DOWNLOAD_DELAY = 5
|
|
@@ -149,21 +149,26 @@ def load_data():
|
|
| 149 |
if 'products' not in data: data['products'] = []
|
| 150 |
if 'categories' not in data: data['categories'] = []
|
| 151 |
if 'orders' not in data: data['orders'] = {}
|
| 152 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
for product in data['products']:
|
|
|
|
|
|
|
| 154 |
if 'prices' not in product or not isinstance(product['prices'], list):
|
| 155 |
-
if 'price' in product:
|
| 156 |
product['prices'] = [{'type': 'шт', 'value': product.pop('price')}]
|
| 157 |
else:
|
| 158 |
product['prices'] = []
|
| 159 |
-
# Ensure prices have type and value and value is float
|
| 160 |
product['prices'] = [p for p in product['prices'] if isinstance(p, dict) and 'type' in p and 'value' in p]
|
| 161 |
for p in product['prices']:
|
| 162 |
try:
|
| 163 |
p['value'] = round(float(p['value']), 2)
|
| 164 |
except (ValueError, TypeError):
|
| 165 |
-
p['value'] = 0.0
|
| 166 |
-
if not product['prices']:
|
| 167 |
product['prices'] = [{'type': 'шт', 'value': 0.0}]
|
| 168 |
|
| 169 |
return data
|
|
@@ -184,10 +189,15 @@ def load_data():
|
|
| 184 |
if 'categories' not in data: data['categories'] = []
|
| 185 |
if 'orders' not in data: data['orders'] = {}
|
| 186 |
|
| 187 |
-
|
|
|
|
|
|
|
|
|
|
| 188 |
for product in data['products']:
|
|
|
|
|
|
|
| 189 |
if 'prices' not in product or not isinstance(product['prices'], list):
|
| 190 |
-
if 'price' in product:
|
| 191 |
product['prices'] = [{'type': 'шт', 'value': product.pop('price')}]
|
| 192 |
else:
|
| 193 |
product['prices'] = []
|
|
@@ -256,12 +266,14 @@ CATALOG_TEMPLATE = '''
|
|
| 256 |
.header { display: flex; justify-content: space-between; align-items: center; padding: 15px 0; border-bottom: 1px solid #e0e0e0; }
|
| 257 |
.header h1 { font-size: 1.8rem; font-weight: 600; color: #e3a84f; }
|
| 258 |
.store-address { padding: 15px; text-align: center; background-color: #f9f9f9; margin: 20px 0; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.03); font-size: 1rem; color: #666; }
|
| 259 |
-
.filters-
|
|
|
|
|
|
|
| 260 |
.search-container { margin: 20px 0; text-align: center; }
|
| 261 |
#search-input { width: 90%; max-width: 600px; padding: 12px 18px; font-size: 1rem; border: 1px solid #e0e0e0; border-radius: 25px; outline: none; box-shadow: 0 2px 5px rgba(0,0,0,0.03); transition: all 0.3s ease; }
|
| 262 |
#search-input:focus { border-color: #e3a84f; box-shadow: 0 0 0 3px rgba(227, 168, 79, 0.15); }
|
| 263 |
-
.category-filter { padding: 8px 16px; border: 1px solid #e0e0e0; border-radius: 20px; background-color: #fff; cursor: pointer; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); font-size: 0.9rem; font-weight: 400; color: #c89345; }
|
| 264 |
-
.category-filter.active, .category-filter:hover { background-color: #e3a84f; color: white; border-color: #e3a84f; box-shadow: 0 2px 10px rgba(227, 168, 79, 0.2); }
|
| 265 |
.products-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 20px; padding: 10px; }
|
| 266 |
@media (min-width: 600px) { .products-grid { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); } }
|
| 267 |
@media (min-width: 900px) { .products-grid { grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); } }
|
|
@@ -328,11 +340,14 @@ CATALOG_TEMPLATE = '''
|
|
| 328 |
|
| 329 |
<div class="store-address">Наш адрес: {{ store_address }}</div>
|
| 330 |
|
| 331 |
-
<div class="filters-
|
| 332 |
-
<
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
|
|
|
|
|
|
|
|
|
| 336 |
</div>
|
| 337 |
|
| 338 |
<div class="search-container">
|
|
@@ -344,7 +359,8 @@ CATALOG_TEMPLATE = '''
|
|
| 344 |
<div class="product"
|
| 345 |
data-name="{{ product['name']|lower }}"
|
| 346 |
data-description="{{ product.get('description', '')|lower }}"
|
| 347 |
-
data-category="{{ product.get('category', 'Без категории') }}"
|
|
|
|
| 348 |
{% if product.get('is_top', False) %}
|
| 349 |
<span class="top-product-indicator"><i class="fas fa-star"></i> Топ</span>
|
| 350 |
{% endif %}
|
|
@@ -437,11 +453,13 @@ CATALOG_TEMPLATE = '''
|
|
| 437 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.js"></script>
|
| 438 |
<script>
|
| 439 |
const products = {{ products|tojson }};
|
|
|
|
| 440 |
const repoId = '{{ repo_id }}';
|
| 441 |
const currencyCode = '{{ currency_code }}';
|
| 442 |
let selectedProductIndex = null;
|
| 443 |
let cart = JSON.parse(localStorage.getItem('mekaCart') || '[]');
|
| 444 |
-
|
|
|
|
| 445 |
|
| 446 |
function openModal(index) {
|
| 447 |
loadProductDetails(index);
|
|
@@ -547,7 +565,7 @@ CATALOG_TEMPLATE = '''
|
|
| 547 |
priceTypeSelect.style.display = 'none';
|
| 548 |
if(priceTypeLabel) priceTypeLabel.style.display = 'none';
|
| 549 |
alert("Для этого товара нет доступных цен.");
|
| 550 |
-
return;
|
| 551 |
}
|
| 552 |
|
| 553 |
|
|
@@ -568,7 +586,7 @@ CATALOG_TEMPLATE = '''
|
|
| 568 |
const color = colorSelect.style.display !== 'none' && colorSelect.value ? colorSelect.value : 'N/A';
|
| 569 |
const priceTypeSelect = document.getElementById('priceTypeSelect');
|
| 570 |
const selectedPriceOption = priceTypeSelect.options[priceTypeSelect.selectedIndex];
|
| 571 |
-
const priceType = selectedPriceOption ? selectedPriceOption.value : 'шт';
|
| 572 |
const priceValue = selectedPriceOption ? parseFloat(selectedPriceOption.dataset.priceValue) : null;
|
| 573 |
|
| 574 |
|
|
@@ -589,7 +607,7 @@ CATALOG_TEMPLATE = '''
|
|
| 589 |
return;
|
| 590 |
}
|
| 591 |
|
| 592 |
-
const cartItemId = `${product.name}-${priceType}-${color}`;
|
| 593 |
const existingItemIndex = cart.findIndex(item => item.id === cartItemId);
|
| 594 |
|
| 595 |
if (existingItemIndex > -1) {
|
|
@@ -598,8 +616,8 @@ CATALOG_TEMPLATE = '''
|
|
| 598 |
cart.push({
|
| 599 |
id: cartItemId,
|
| 600 |
name: product.name,
|
| 601 |
-
price_type: priceType,
|
| 602 |
-
price_value: priceValue,
|
| 603 |
photo: product.photos && product.photos.length > 0 ? product.photos[0] : null,
|
| 604 |
quantity: quantity,
|
| 605 |
color: color
|
|
@@ -641,13 +659,13 @@ CATALOG_TEMPLATE = '''
|
|
| 641 |
cartTotalElement.textContent = '0.00';
|
| 642 |
} else {
|
| 643 |
cartContent.innerHTML = cart.map(item => {
|
| 644 |
-
const itemTotal = item.price_value * item.quantity;
|
| 645 |
total += itemTotal;
|
| 646 |
const photoUrl = item.photo
|
| 647 |
? `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${item.photo}`
|
| 648 |
: 'https://via.placeholder.com/60x60.png?text=N/A';
|
| 649 |
const colorText = item.color && item.color !== 'N/A' ? ` (Цвет: ${item.color})` : '';
|
| 650 |
-
const priceTypeText = item.price_type ? `/${item.price_type}` : '';
|
| 651 |
|
| 652 |
return `
|
| 653 |
<div class="cart-item">
|
|
@@ -733,8 +751,6 @@ CATALOG_TEMPLATE = '''
|
|
| 733 |
|
| 734 |
function filterProducts() {
|
| 735 |
const searchTerm = document.getElementById('search-input').value.toLowerCase().trim();
|
| 736 |
-
const activeCategoryButton = document.querySelector('.category-filter.active');
|
| 737 |
-
const activeCategory = activeCategoryButton ? activeCategoryButton.dataset.category : 'all';
|
| 738 |
const grid = document.getElementById('products-grid');
|
| 739 |
let visibleProducts = 0;
|
| 740 |
|
|
@@ -745,11 +761,13 @@ CATALOG_TEMPLATE = '''
|
|
| 745 |
const name = productElement.getAttribute('data-name');
|
| 746 |
const description = productElement.getAttribute('data-description');
|
| 747 |
const category = productElement.getAttribute('data-category');
|
|
|
|
| 748 |
|
| 749 |
const matchesSearch = !searchTerm || name.includes(searchTerm) || description.includes(searchTerm);
|
| 750 |
const matchesCategory = activeCategory === 'all' || category === activeCategory;
|
|
|
|
| 751 |
|
| 752 |
-
if (matchesSearch && matchesCategory) {
|
| 753 |
productElement.style.display = 'flex';
|
| 754 |
visibleProducts++;
|
| 755 |
} else {
|
|
@@ -769,6 +787,38 @@ CATALOG_TEMPLATE = '''
|
|
| 769 |
grid.appendChild(p);
|
| 770 |
}
|
| 771 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 772 |
|
| 773 |
function setupFilters() {
|
| 774 |
const searchInput = document.getElementById('search-input');
|
|
@@ -780,6 +830,9 @@ CATALOG_TEMPLATE = '''
|
|
| 780 |
filter.addEventListener('click', function() {
|
| 781 |
categoryFilters.forEach(f => f.classList.remove('active'));
|
| 782 |
this.classList.add('active');
|
|
|
|
|
|
|
|
|
|
| 783 |
filterProducts();
|
| 784 |
});
|
| 785 |
});
|
|
@@ -869,7 +922,7 @@ PRODUCT_DETAIL_TEMPLATE = '''
|
|
| 869 |
</div>
|
| 870 |
|
| 871 |
<div style="margin-top: 20px; font-size: 1rem; line-height: 1.7; color: #333;">
|
| 872 |
-
<p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
|
| 873 |
<p style="font-size: 1.2rem; font-weight: bold; color: #c89345; margin-bottom: 10px;"><strong>Цена:</strong></p>
|
| 874 |
<div class="price-list" style="text-align: left;">
|
| 875 |
{% if product.get('prices') %}
|
|
@@ -889,9 +942,8 @@ PRODUCT_DETAIL_TEMPLATE = '''
|
|
| 889 |
</div>
|
| 890 |
</div>
|
| 891 |
<style>
|
| 892 |
-
/* Add Swiper Modal Specific Styles if needed */
|
| 893 |
#productModal .swiper-button-next, #productModal .swiper-button-prev {
|
| 894 |
-
color: #e3a84f;
|
| 895 |
}
|
| 896 |
</style>
|
| 897 |
'''
|
|
@@ -976,7 +1028,7 @@ ORDER_TEMPLATE = '''
|
|
| 976 |
function sendOrderViaWhatsApp() {
|
| 977 |
const orderId = '{{ order.id }}';
|
| 978 |
const orderUrl = `{{ request.url }}`;
|
| 979 |
-
const whatsappNumber = "{{ whatsapp_number }}";
|
| 980 |
|
| 981 |
let message = `Здравствуйте! Хочу подтвердить свой заказ на сайте "Мир праздника":%0A%0A`;
|
| 982 |
message += `*Номер заказа:* ${orderId}%0A`;
|
|
@@ -1015,6 +1067,7 @@ ADMIN_TEMPLATE = '''
|
|
| 1015 |
h1 { font-size: 1.8rem; }
|
| 1016 |
h2 { font-size: 1.5rem; margin-top: 30px; display: flex; align-items: center; gap: 8px; }
|
| 1017 |
h3 { font-size: 1.2rem; color: #c89345; margin-top: 20px; }
|
|
|
|
| 1018 |
.section { margin-bottom: 30px; padding: 20px; background-color: #f9f9f9; border: 1px solid #e0e0e0; border-radius: 8px; }
|
| 1019 |
form { margin-bottom: 20px; }
|
| 1020 |
label { font-weight: 500; margin-top: 10px; display: block; color: #666; font-size: 0.9rem;}
|
|
@@ -1039,8 +1092,8 @@ ADMIN_TEMPLATE = '''
|
|
| 1039 |
.item strong { color: #333; }
|
| 1040 |
.item .description { font-size: 0.85rem; color: #999; max-height: 60px; overflow: hidden; text-overflow: ellipsis; }
|
| 1041 |
.item-actions { margin-top: 15px; display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
|
| 1042 |
-
.item-actions button:not(.delete-button) { background-color: #e3a84f; }
|
| 1043 |
-
.item-actions button:not(.delete-button):hover { background-color: #c89345; }
|
| 1044 |
.edit-form-container { margin-top: 15px; padding: 20px; background: #fffcf5; border: 1px dashed #e0e0e0; border-radius: 6px; display: none; }
|
| 1045 |
details { background-color: #f9f9f9; border: 1px solid #e0e0e0; border-radius: 8px; margin-bottom: 20px; }
|
| 1046 |
details > summary { cursor: pointer; font-weight: 600; color: #c89345; display: block; padding: 15px; border-bottom: 1px solid #e0e0e0; list-style: none; position: relative; }
|
|
@@ -1050,10 +1103,9 @@ ADMIN_TEMPLATE = '''
|
|
| 1050 |
details .form-content { padding: 20px; }
|
| 1051 |
.color-input-group, .price-input-group { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
|
| 1052 |
.color-input-group input, .price-input-group input { flex-grow: 1; margin: 0; }
|
| 1053 |
-
.price-input-group input[type="text"] { width: 100px; flex-grow: 0; }
|
| 1054 |
-
.price-input-group input[type="number"] { flex-grow: 1; }
|
| 1055 |
-
.price-input-group label { margin-top: 0; width: auto; display: inline-block; font-weight: normal; color: #333;}
|
| 1056 |
-
|
| 1057 |
.remove-color-btn, .remove-price-btn { background-color: #dc3545; padding: 6px 10px; font-size: 0.8rem; margin-top: 0; line-height: 1; }
|
| 1058 |
.remove-color-btn:hover, .remove-price-btn:hover { background-color: #c82333; }
|
| 1059 |
.add-color-btn, .add-price-btn { background-color: #f0c38b; color: #c89345; border: 1px solid #e0e0e0; }
|
|
@@ -1072,8 +1124,10 @@ ADMIN_TEMPLATE = '''
|
|
| 1072 |
.status-indicator.in-stock { background-color: #d4edda; color: #155724; }
|
| 1073 |
.status-indicator.out-of-stock { background-color: #f8d7da; color: #721c24; }
|
| 1074 |
.status-indicator.top-product { background-color: #fff3cd; color: #856404; margin-left: 5px;}
|
| 1075 |
-
|
| 1076 |
-
|
|
|
|
|
|
|
| 1077 |
</style>
|
| 1078 |
</head>
|
| 1079 |
<body>
|
|
@@ -1111,7 +1165,7 @@ ADMIN_TEMPLATE = '''
|
|
| 1111 |
<div class="section">
|
| 1112 |
<h2><i class="fas fa-tags"></i> Управление категориями</h2>
|
| 1113 |
<details>
|
| 1114 |
-
<summary><i class="fas fa-plus-circle"></i> Добавить новую категорию</summary>
|
| 1115 |
<div class="form-content">
|
| 1116 |
<form method="POST">
|
| 1117 |
<input type="hidden" name="action" value="add_category">
|
|
@@ -1126,13 +1180,40 @@ ADMIN_TEMPLATE = '''
|
|
| 1126 |
{% if categories %}
|
| 1127 |
<div class="item-list">
|
| 1128 |
{% for category in categories %}
|
| 1129 |
-
<div class="item"
|
| 1130 |
-
|
| 1131 |
-
|
| 1132 |
-
|
| 1133 |
-
|
| 1134 |
-
|
| 1135 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1136 |
</div>
|
| 1137 |
{% endfor %}
|
| 1138 |
</div>
|
|
@@ -1176,13 +1257,20 @@ ADMIN_TEMPLATE = '''
|
|
| 1176 |
|
| 1177 |
<label for="add_description" style="margin-top: 15px;">Описание:</label>
|
| 1178 |
<textarea id="add_description" name="description" rows="4"></textarea>
|
|
|
|
| 1179 |
<label for="add_category">Категория:</label>
|
| 1180 |
-
<select id="add_category" name="category">
|
| 1181 |
<option value="Без категории">Без категории</option>
|
| 1182 |
{% for category in categories %}
|
| 1183 |
-
<option value="{{ category }}">{{ category }}</option>
|
| 1184 |
{% endfor %}
|
| 1185 |
</select>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1186 |
<label for="add_photos">Фотографии (до 10 шт.):</label>
|
| 1187 |
<input type="file" id="add_photos" name="photos" accept="image/*" multiple>
|
| 1188 |
<label>Цвета/Варианты (оставьте пустым, если нет):</label>
|
|
@@ -1235,7 +1323,7 @@ ADMIN_TEMPLATE = '''
|
|
| 1235 |
<span class="status-indicator top-product"><i class="fas fa-star"></i> Топ</span>
|
| 1236 |
{% endif %}
|
| 1237 |
</h3>
|
| 1238 |
-
<p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
|
| 1239 |
<p><strong>Цены:</strong></p>
|
| 1240 |
<div class="admin-price-list">
|
| 1241 |
{% if product.get('prices') %}
|
|
@@ -1301,13 +1389,20 @@ ADMIN_TEMPLATE = '''
|
|
| 1301 |
|
| 1302 |
<label style="margin-top: 15px;">Описание:</label>
|
| 1303 |
<textarea name="description" rows="4">{{ product.get('description', '') }}</textarea>
|
|
|
|
| 1304 |
<label>Категория:</label>
|
| 1305 |
-
<select name="category">
|
| 1306 |
<option value="Без категории" {% if product.get('category', 'Без категории') == 'Без категории' %}selected{% endif %}>Без категории</option>
|
| 1307 |
{% for category in categories %}
|
| 1308 |
-
<option value="{{ category }}" {% if product.get('category') == category %}selected{% endif %}>{{ category }}</option>
|
| 1309 |
{% endfor %}
|
| 1310 |
</select>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1311 |
<label>Заменить фотографии (выберите новые файлы, до 10 шт.):</label>
|
| 1312 |
<input type="file" name="photos" accept="image/*" multiple>
|
| 1313 |
{% if product.get('photos') %}
|
|
@@ -1362,10 +1457,42 @@ ADMIN_TEMPLATE = '''
|
|
| 1362 |
</div>
|
| 1363 |
|
| 1364 |
<script>
|
|
|
|
|
|
|
| 1365 |
function toggleEditForm(formId) {
|
| 1366 |
const formContainer = document.getElementById(formId);
|
| 1367 |
if (formContainer) {
|
| 1368 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1369 |
}
|
| 1370 |
}
|
| 1371 |
|
|
@@ -1391,10 +1518,6 @@ ADMIN_TEMPLATE = '''
|
|
| 1391 |
if (group) {
|
| 1392 |
const container = group.parentNode;
|
| 1393 |
group.remove();
|
| 1394 |
-
// Optional: Add a placeholder if the last input is removed
|
| 1395 |
-
// if (container && container.children.length === 0) {
|
| 1396 |
-
// addColorInput(container.id);
|
| 1397 |
-
// }
|
| 1398 |
} else {
|
| 1399 |
console.warn("Could not find parent .color-input-group for remove button");
|
| 1400 |
}
|
|
@@ -1424,7 +1547,6 @@ ADMIN_TEMPLATE = '''
|
|
| 1424 |
const group = button.closest('.price-input-group');
|
| 1425 |
if (group) {
|
| 1426 |
const container = group.parentNode;
|
| 1427 |
-
// Prevent removing the last price input
|
| 1428 |
if (container.children.length <= 1) {
|
| 1429 |
alert("Должен быть указан хотя бы один вариант цены.");
|
| 1430 |
return;
|
|
@@ -1435,14 +1557,21 @@ ADMIN_TEMPLATE = '''
|
|
| 1435 |
}
|
| 1436 |
}
|
| 1437 |
|
| 1438 |
-
// Initial add price input if none exist on load for Add form
|
| 1439 |
document.addEventListener('DOMContentLoaded', () => {
|
| 1440 |
const addPriceInputsDiv = document.getElementById('add-price-inputs');
|
| 1441 |
if(addPriceInputsDiv && addPriceInputsDiv.children.length === 0) {
|
| 1442 |
addPriceInput('add-price-inputs');
|
| 1443 |
}
|
| 1444 |
-
});
|
| 1445 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1446 |
</script>
|
| 1447 |
</body>
|
| 1448 |
</html>
|
|
@@ -1453,7 +1582,7 @@ ADMIN_TEMPLATE = '''
|
|
| 1453 |
def catalog():
|
| 1454 |
data = load_data()
|
| 1455 |
all_products = data.get('products', [])
|
| 1456 |
-
categories = sorted(data.get('categories', []))
|
| 1457 |
|
| 1458 |
products_in_stock = [p for p in all_products if p.get('in_stock', True)]
|
| 1459 |
products_sorted = sorted(products_in_stock, key=lambda p: (not p.get('is_top', False), p.get('name', '').lower()))
|
|
@@ -1500,7 +1629,6 @@ def create_order():
|
|
| 1500 |
total_price = 0
|
| 1501 |
processed_cart = []
|
| 1502 |
for item in cart_items:
|
| 1503 |
-
# Expect price_value, price_type, quantity, name, color, photo
|
| 1504 |
if not all(k in item for k in ('name', 'price_value', 'quantity', 'price_type')):
|
| 1505 |
logging.error(f"Invalid cart item structure received: {item}")
|
| 1506 |
return jsonify({"error": "Неверный формат товара в корзине."}), 400
|
|
@@ -1587,9 +1715,9 @@ def admin():
|
|
| 1587 |
try:
|
| 1588 |
if action == 'add_category':
|
| 1589 |
category_name = request.form.get('category_name', '').strip()
|
| 1590 |
-
if category_name and category_name
|
| 1591 |
-
categories.append(category_name)
|
| 1592 |
-
data['categories'] = categories
|
| 1593 |
save_data(data)
|
| 1594 |
logging.info(f"Category '{category_name}' added.")
|
| 1595 |
flash(f"Категория '{category_name}' успешно добавлена.", 'success')
|
|
@@ -1600,14 +1728,33 @@ def admin():
|
|
| 1600 |
logging.warning(f"Category '{category_name}' already exists.")
|
| 1601 |
flash(f"Категория '{category_name}' уже существует.", 'error')
|
| 1602 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1603 |
elif action == 'delete_category':
|
| 1604 |
category_to_delete = request.form.get('category_name')
|
| 1605 |
-
|
| 1606 |
-
|
|
|
|
| 1607 |
updated_count = 0
|
| 1608 |
for product in products:
|
| 1609 |
if product.get('category') == category_to_delete:
|
| 1610 |
product['category'] = 'Без категории'
|
|
|
|
| 1611 |
updated_count += 1
|
| 1612 |
data['categories'] = categories
|
| 1613 |
data['products'] = products
|
|
@@ -1615,13 +1762,30 @@ def admin():
|
|
| 1615 |
logging.info(f"Category '{category_to_delete}' deleted. Updated products: {updated_count}.")
|
| 1616 |
flash(f"Категория '{category_to_delete}' удалена. {updated_count} товаров обновлено.", 'success')
|
| 1617 |
else:
|
| 1618 |
-
logging.warning(f"Attempted to delete non-existent or empty category: {category_to_delete}")
|
| 1619 |
flash(f"Не удалось удалить категорию '{category_to_delete}'.", 'error')
|
| 1620 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1621 |
elif action == 'add_product':
|
| 1622 |
name = request.form.get('name', '').strip()
|
| 1623 |
description = request.form.get('description', '').strip()
|
| 1624 |
category = request.form.get('category')
|
|
|
|
| 1625 |
photos_files = request.files.getlist('photos')
|
| 1626 |
colors = [c.strip() for c in request.form.getlist('colors') if c.strip()]
|
| 1627 |
in_stock = 'in_stock' in request.form
|
|
@@ -1635,7 +1799,7 @@ def admin():
|
|
| 1635 |
type_str = type_str.strip()
|
| 1636 |
value_str = value_str.strip().replace(',', '.')
|
| 1637 |
if not type_str or not value_str:
|
| 1638 |
-
continue
|
| 1639 |
|
| 1640 |
try:
|
| 1641 |
price_value = round(float(value_str), 2)
|
|
@@ -1645,7 +1809,6 @@ def admin():
|
|
| 1645 |
except ValueError:
|
| 1646 |
logging.warning(f"Skipping invalid price value '{value_str}' for type '{type_str}' during add product.")
|
| 1647 |
|
| 1648 |
-
|
| 1649 |
if not name:
|
| 1650 |
flash("Название товара обязательно.", 'error')
|
| 1651 |
return redirect(url_for('admin'))
|
|
@@ -1710,7 +1873,8 @@ def admin():
|
|
| 1710 |
|
| 1711 |
new_product = {
|
| 1712 |
'name': name, 'prices': prices, 'description': description,
|
| 1713 |
-
'category': category if category in categories else 'Без категории',
|
|
|
|
| 1714 |
'photos': photos_list, 'colors': colors,
|
| 1715 |
'in_stock': in_stock, 'is_top': is_top
|
| 1716 |
}
|
|
@@ -1741,7 +1905,9 @@ def admin():
|
|
| 1741 |
product_to_edit['name'] = request.form.get('name', product_to_edit['name']).strip()
|
| 1742 |
product_to_edit['description'] = request.form.get('description', product_to_edit.get('description', '')).strip()
|
| 1743 |
category = request.form.get('category')
|
| 1744 |
-
|
|
|
|
|
|
|
| 1745 |
product_to_edit['colors'] = [c.strip() for c in request.form.getlist('colors') if c.strip()]
|
| 1746 |
product_to_edit['in_stock'] = 'in_stock' in request.form
|
| 1747 |
product_to_edit['is_top'] = 'is_top' in request.form
|
|
@@ -1754,7 +1920,7 @@ def admin():
|
|
| 1754 |
type_str = type_str.strip()
|
| 1755 |
value_str = value_str.strip().replace(',', '.')
|
| 1756 |
if not type_str or not value_str:
|
| 1757 |
-
continue
|
| 1758 |
|
| 1759 |
try:
|
| 1760 |
price_value = round(float(value_str), 2)
|
|
@@ -1904,7 +2070,7 @@ def admin():
|
|
| 1904 |
|
| 1905 |
current_data = load_data()
|
| 1906 |
display_products = sorted(current_data.get('products', []), key=lambda p: p.get('name', '').lower())
|
| 1907 |
-
display_categories = sorted(current_data.get('categories', []))
|
| 1908 |
|
| 1909 |
return render_template_string(
|
| 1910 |
ADMIN_TEMPLATE,
|
|
@@ -1956,4 +2122,3 @@ if __name__ == '__main__':
|
|
| 1956 |
port = int(os.environ.get('PORT', 7860))
|
| 1957 |
logging.info(f"Starting Flask app on host 0.0.0.0 and port {port}")
|
| 1958 |
app.run(debug=False, host='0.0.0.0', port=port)
|
| 1959 |
-
|
|
|
|
| 21 |
|
| 22 |
SYNC_FILES = [DATA_FILE]
|
| 23 |
|
| 24 |
+
REPO_ID = "Kgshop/prazdnik"
|
| 25 |
HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
|
| 26 |
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
|
| 27 |
|
|
|
|
| 30 |
CURRENCY_CODE = 'KGS'
|
| 31 |
CURRENCY_NAME = 'Кыргызский сом'
|
| 32 |
|
| 33 |
+
WHATSAPP_NUMBER = "996702888188"
|
| 34 |
|
| 35 |
DOWNLOAD_RETRIES = 3
|
| 36 |
DOWNLOAD_DELAY = 5
|
|
|
|
| 149 |
if 'products' not in data: data['products'] = []
|
| 150 |
if 'categories' not in data: data['categories'] = []
|
| 151 |
if 'orders' not in data: data['orders'] = {}
|
| 152 |
+
|
| 153 |
+
if 'categories' in data and data['categories'] and isinstance(data['categories'][0], str):
|
| 154 |
+
logging.info("Old category format detected. Migrating to new structure.")
|
| 155 |
+
data['categories'] = [{'name': c, 'subcategories': []} for c in data['categories']]
|
| 156 |
+
|
| 157 |
for product in data['products']:
|
| 158 |
+
if 'subcategory' not in product:
|
| 159 |
+
product['subcategory'] = None
|
| 160 |
if 'prices' not in product or not isinstance(product['prices'], list):
|
| 161 |
+
if 'price' in product:
|
| 162 |
product['prices'] = [{'type': 'шт', 'value': product.pop('price')}]
|
| 163 |
else:
|
| 164 |
product['prices'] = []
|
|
|
|
| 165 |
product['prices'] = [p for p in product['prices'] if isinstance(p, dict) and 'type' in p and 'value' in p]
|
| 166 |
for p in product['prices']:
|
| 167 |
try:
|
| 168 |
p['value'] = round(float(p['value']), 2)
|
| 169 |
except (ValueError, TypeError):
|
| 170 |
+
p['value'] = 0.0
|
| 171 |
+
if not product['prices']:
|
| 172 |
product['prices'] = [{'type': 'шт', 'value': 0.0}]
|
| 173 |
|
| 174 |
return data
|
|
|
|
| 189 |
if 'categories' not in data: data['categories'] = []
|
| 190 |
if 'orders' not in data: data['orders'] = {}
|
| 191 |
|
| 192 |
+
if 'categories' in data and data['categories'] and isinstance(data['categories'][0], str):
|
| 193 |
+
logging.info("Old category format detected after download. Migrating to new structure.")
|
| 194 |
+
data['categories'] = [{'name': c, 'subcategories': []} for c in data['categories']]
|
| 195 |
+
|
| 196 |
for product in data['products']:
|
| 197 |
+
if 'subcategory' not in product:
|
| 198 |
+
product['subcategory'] = None
|
| 199 |
if 'prices' not in product or not isinstance(product['prices'], list):
|
| 200 |
+
if 'price' in product:
|
| 201 |
product['prices'] = [{'type': 'шт', 'value': product.pop('price')}]
|
| 202 |
else:
|
| 203 |
product['prices'] = []
|
|
|
|
| 266 |
.header { display: flex; justify-content: space-between; align-items: center; padding: 15px 0; border-bottom: 1px solid #e0e0e0; }
|
| 267 |
.header h1 { font-size: 1.8rem; font-weight: 600; color: #e3a84f; }
|
| 268 |
.store-address { padding: 15px; text-align: center; background-color: #f9f9f9; margin: 20px 0; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.03); font-size: 1rem; color: #666; }
|
| 269 |
+
.filters-wrapper { margin: 20px 0; }
|
| 270 |
+
.filters-container, .sub-filters-container { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }
|
| 271 |
+
.sub-filters-container { margin-top: 15px; padding-top: 15px; border-top: 1px dashed #e0e0e0;}
|
| 272 |
.search-container { margin: 20px 0; text-align: center; }
|
| 273 |
#search-input { width: 90%; max-width: 600px; padding: 12px 18px; font-size: 1rem; border: 1px solid #e0e0e0; border-radius: 25px; outline: none; box-shadow: 0 2px 5px rgba(0,0,0,0.03); transition: all 0.3s ease; }
|
| 274 |
#search-input:focus { border-color: #e3a84f; box-shadow: 0 0 0 3px rgba(227, 168, 79, 0.15); }
|
| 275 |
+
.category-filter, .subcategory-filter { padding: 8px 16px; border: 1px solid #e0e0e0; border-radius: 20px; background-color: #fff; cursor: pointer; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); font-size: 0.9rem; font-weight: 400; color: #c89345; }
|
| 276 |
+
.category-filter.active, .category-filter:hover, .subcategory-filter.active, .subcategory-filter:hover { background-color: #e3a84f; color: white; border-color: #e3a84f; box-shadow: 0 2px 10px rgba(227, 168, 79, 0.2); }
|
| 277 |
.products-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 20px; padding: 10px; }
|
| 278 |
@media (min-width: 600px) { .products-grid { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); } }
|
| 279 |
@media (min-width: 900px) { .products-grid { grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); } }
|
|
|
|
| 340 |
|
| 341 |
<div class="store-address">Наш адрес: {{ store_address }}</div>
|
| 342 |
|
| 343 |
+
<div class="filters-wrapper">
|
| 344 |
+
<div class="filters-container">
|
| 345 |
+
<button class="category-filter active" data-category="all">Все категории</button>
|
| 346 |
+
{% for category in categories %}
|
| 347 |
+
<button class="category-filter" data-category="{{ category.name }}">{{ category.name }}</button>
|
| 348 |
+
{% endfor %}
|
| 349 |
+
</div>
|
| 350 |
+
<div class="sub-filters-container" id="sub-filters-container"></div>
|
| 351 |
</div>
|
| 352 |
|
| 353 |
<div class="search-container">
|
|
|
|
| 359 |
<div class="product"
|
| 360 |
data-name="{{ product['name']|lower }}"
|
| 361 |
data-description="{{ product.get('description', '')|lower }}"
|
| 362 |
+
data-category="{{ product.get('category', 'Без категории') }}"
|
| 363 |
+
data-subcategory="{{ product.get('subcategory', '')|lower if product.get('subcategory') else '' }}">
|
| 364 |
{% if product.get('is_top', False) %}
|
| 365 |
<span class="top-product-indicator"><i class="fas fa-star"></i> Топ</span>
|
| 366 |
{% endif %}
|
|
|
|
| 453 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.js"></script>
|
| 454 |
<script>
|
| 455 |
const products = {{ products|tojson }};
|
| 456 |
+
const categoriesData = {{ categories|tojson }};
|
| 457 |
const repoId = '{{ repo_id }}';
|
| 458 |
const currencyCode = '{{ currency_code }}';
|
| 459 |
let selectedProductIndex = null;
|
| 460 |
let cart = JSON.parse(localStorage.getItem('mekaCart') || '[]');
|
| 461 |
+
let activeCategory = 'all';
|
| 462 |
+
let activeSubcategory = 'all';
|
| 463 |
|
| 464 |
function openModal(index) {
|
| 465 |
loadProductDetails(index);
|
|
|
|
| 565 |
priceTypeSelect.style.display = 'none';
|
| 566 |
if(priceTypeLabel) priceTypeLabel.style.display = 'none';
|
| 567 |
alert("Для этого товара нет доступных цен.");
|
| 568 |
+
return;
|
| 569 |
}
|
| 570 |
|
| 571 |
|
|
|
|
| 586 |
const color = colorSelect.style.display !== 'none' && colorSelect.value ? colorSelect.value : 'N/A';
|
| 587 |
const priceTypeSelect = document.getElementById('priceTypeSelect');
|
| 588 |
const selectedPriceOption = priceTypeSelect.options[priceTypeSelect.selectedIndex];
|
| 589 |
+
const priceType = selectedPriceOption ? selectedPriceOption.value : 'шт';
|
| 590 |
const priceValue = selectedPriceOption ? parseFloat(selectedPriceOption.dataset.priceValue) : null;
|
| 591 |
|
| 592 |
|
|
|
|
| 607 |
return;
|
| 608 |
}
|
| 609 |
|
| 610 |
+
const cartItemId = `${product.name}-${priceType}-${color}`;
|
| 611 |
const existingItemIndex = cart.findIndex(item => item.id === cartItemId);
|
| 612 |
|
| 613 |
if (existingItemIndex > -1) {
|
|
|
|
| 616 |
cart.push({
|
| 617 |
id: cartItemId,
|
| 618 |
name: product.name,
|
| 619 |
+
price_type: priceType,
|
| 620 |
+
price_value: priceValue,
|
| 621 |
photo: product.photos && product.photos.length > 0 ? product.photos[0] : null,
|
| 622 |
quantity: quantity,
|
| 623 |
color: color
|
|
|
|
| 659 |
cartTotalElement.textContent = '0.00';
|
| 660 |
} else {
|
| 661 |
cartContent.innerHTML = cart.map(item => {
|
| 662 |
+
const itemTotal = item.price_value * item.quantity;
|
| 663 |
total += itemTotal;
|
| 664 |
const photoUrl = item.photo
|
| 665 |
? `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${item.photo}`
|
| 666 |
: 'https://via.placeholder.com/60x60.png?text=N/A';
|
| 667 |
const colorText = item.color && item.color !== 'N/A' ? ` (Цвет: ${item.color})` : '';
|
| 668 |
+
const priceTypeText = item.price_type ? `/${item.price_type}` : '';
|
| 669 |
|
| 670 |
return `
|
| 671 |
<div class="cart-item">
|
|
|
|
| 751 |
|
| 752 |
function filterProducts() {
|
| 753 |
const searchTerm = document.getElementById('search-input').value.toLowerCase().trim();
|
|
|
|
|
|
|
| 754 |
const grid = document.getElementById('products-grid');
|
| 755 |
let visibleProducts = 0;
|
| 756 |
|
|
|
|
| 761 |
const name = productElement.getAttribute('data-name');
|
| 762 |
const description = productElement.getAttribute('data-description');
|
| 763 |
const category = productElement.getAttribute('data-category');
|
| 764 |
+
const subcategory = productElement.getAttribute('data-subcategory');
|
| 765 |
|
| 766 |
const matchesSearch = !searchTerm || name.includes(searchTerm) || description.includes(searchTerm);
|
| 767 |
const matchesCategory = activeCategory === 'all' || category === activeCategory;
|
| 768 |
+
const matchesSubcategory = activeSubcategory === 'all' || subcategory === activeSubcategory;
|
| 769 |
|
| 770 |
+
if (matchesSearch && matchesCategory && matchesSubcategory) {
|
| 771 |
productElement.style.display = 'flex';
|
| 772 |
visibleProducts++;
|
| 773 |
} else {
|
|
|
|
| 787 |
grid.appendChild(p);
|
| 788 |
}
|
| 789 |
}
|
| 790 |
+
|
| 791 |
+
function renderSubcategories() {
|
| 792 |
+
const subFiltersContainer = document.getElementById('sub-filters-container');
|
| 793 |
+
subFiltersContainer.innerHTML = '';
|
| 794 |
+
|
| 795 |
+
if (activeCategory === 'all') {
|
| 796 |
+
subFiltersContainer.style.display = 'none';
|
| 797 |
+
return;
|
| 798 |
+
}
|
| 799 |
+
|
| 800 |
+
const categoryData = categoriesData.find(c => c.name === activeCategory);
|
| 801 |
+
if (categoryData && categoryData.subcategories && categoryData.subcategories.length > 0) {
|
| 802 |
+
subFiltersContainer.style.display = 'flex';
|
| 803 |
+
|
| 804 |
+
let buttonsHTML = `<button class="subcategory-filter active" data-subcategory="all">Все в "${activeCategory}"</button>`;
|
| 805 |
+
categoryData.subcategories.forEach(sub => {
|
| 806 |
+
buttonsHTML += `<button class="subcategory-filter" data-subcategory="${sub.toLowerCase()}">${sub}</button>`;
|
| 807 |
+
});
|
| 808 |
+
subFiltersContainer.innerHTML = buttonsHTML;
|
| 809 |
+
|
| 810 |
+
subFiltersContainer.querySelectorAll('.subcategory-filter').forEach(subFilter => {
|
| 811 |
+
subFilter.addEventListener('click', function() {
|
| 812 |
+
subFiltersContainer.querySelectorAll('.subcategory-filter').forEach(f => f.classList.remove('active'));
|
| 813 |
+
this.classList.add('active');
|
| 814 |
+
activeSubcategory = this.dataset.subcategory;
|
| 815 |
+
filterProducts();
|
| 816 |
+
});
|
| 817 |
+
});
|
| 818 |
+
} else {
|
| 819 |
+
subFiltersContainer.style.display = 'none';
|
| 820 |
+
}
|
| 821 |
+
}
|
| 822 |
|
| 823 |
function setupFilters() {
|
| 824 |
const searchInput = document.getElementById('search-input');
|
|
|
|
| 830 |
filter.addEventListener('click', function() {
|
| 831 |
categoryFilters.forEach(f => f.classList.remove('active'));
|
| 832 |
this.classList.add('active');
|
| 833 |
+
activeCategory = this.dataset.category;
|
| 834 |
+
activeSubcategory = 'all';
|
| 835 |
+
renderSubcategories();
|
| 836 |
filterProducts();
|
| 837 |
});
|
| 838 |
});
|
|
|
|
| 922 |
</div>
|
| 923 |
|
| 924 |
<div style="margin-top: 20px; font-size: 1rem; line-height: 1.7; color: #333;">
|
| 925 |
+
<p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}{% if product.get('subcategory') %} / {{ product.get('subcategory') }}{% endif %}</p>
|
| 926 |
<p style="font-size: 1.2rem; font-weight: bold; color: #c89345; margin-bottom: 10px;"><strong>Цена:</strong></p>
|
| 927 |
<div class="price-list" style="text-align: left;">
|
| 928 |
{% if product.get('prices') %}
|
|
|
|
| 942 |
</div>
|
| 943 |
</div>
|
| 944 |
<style>
|
|
|
|
| 945 |
#productModal .swiper-button-next, #productModal .swiper-button-prev {
|
| 946 |
+
color: #e3a84f;
|
| 947 |
}
|
| 948 |
</style>
|
| 949 |
'''
|
|
|
|
| 1028 |
function sendOrderViaWhatsApp() {
|
| 1029 |
const orderId = '{{ order.id }}';
|
| 1030 |
const orderUrl = `{{ request.url }}`;
|
| 1031 |
+
const whatsappNumber = "{{ whatsapp_number }}";
|
| 1032 |
|
| 1033 |
let message = `Здравствуйте! Хочу подтвердить свой заказ на сайте "Мир праздника":%0A%0A`;
|
| 1034 |
message += `*Номер заказа:* ${orderId}%0A`;
|
|
|
|
| 1067 |
h1 { font-size: 1.8rem; }
|
| 1068 |
h2 { font-size: 1.5rem; margin-top: 30px; display: flex; align-items: center; gap: 8px; }
|
| 1069 |
h3 { font-size: 1.2rem; color: #c89345; margin-top: 20px; }
|
| 1070 |
+
h4 { font-size: 1.1rem; color: #333; margin-top: 20px; }
|
| 1071 |
.section { margin-bottom: 30px; padding: 20px; background-color: #f9f9f9; border: 1px solid #e0e0e0; border-radius: 8px; }
|
| 1072 |
form { margin-bottom: 20px; }
|
| 1073 |
label { font-weight: 500; margin-top: 10px; display: block; color: #666; font-size: 0.9rem;}
|
|
|
|
| 1092 |
.item strong { color: #333; }
|
| 1093 |
.item .description { font-size: 0.85rem; color: #999; max-height: 60px; overflow: hidden; text-overflow: ellipsis; }
|
| 1094 |
.item-actions { margin-top: 15px; display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
|
| 1095 |
+
.item-actions button:not(.delete-button) { background-color: #e3a84f; }
|
| 1096 |
+
.item-actions button:not(.delete-button):hover { background-color: #c89345; }
|
| 1097 |
.edit-form-container { margin-top: 15px; padding: 20px; background: #fffcf5; border: 1px dashed #e0e0e0; border-radius: 6px; display: none; }
|
| 1098 |
details { background-color: #f9f9f9; border: 1px solid #e0e0e0; border-radius: 8px; margin-bottom: 20px; }
|
| 1099 |
details > summary { cursor: pointer; font-weight: 600; color: #c89345; display: block; padding: 15px; border-bottom: 1px solid #e0e0e0; list-style: none; position: relative; }
|
|
|
|
| 1103 |
details .form-content { padding: 20px; }
|
| 1104 |
.color-input-group, .price-input-group { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
|
| 1105 |
.color-input-group input, .price-input-group input { flex-grow: 1; margin: 0; }
|
| 1106 |
+
.price-input-group input[type="text"] { width: 100px; flex-grow: 0; }
|
| 1107 |
+
.price-input-group input[type="number"] { flex-grow: 1; }
|
| 1108 |
+
.price-input-group label { margin-top: 0; width: auto; display: inline-block; font-weight: normal; color: #333;}
|
|
|
|
| 1109 |
.remove-color-btn, .remove-price-btn { background-color: #dc3545; padding: 6px 10px; font-size: 0.8rem; margin-top: 0; line-height: 1; }
|
| 1110 |
.remove-color-btn:hover, .remove-price-btn:hover { background-color: #c82333; }
|
| 1111 |
.add-color-btn, .add-price-btn { background-color: #f0c38b; color: #c89345; border: 1px solid #e0e0e0; }
|
|
|
|
| 1124 |
.status-indicator.in-stock { background-color: #d4edda; color: #155724; }
|
| 1125 |
.status-indicator.out-of-stock { background-color: #f8d7da; color: #721c24; }
|
| 1126 |
.status-indicator.top-product { background-color: #fff3cd; color: #856404; margin-left: 5px;}
|
| 1127 |
+
.admin-price-list { margin-top: 5px; }
|
| 1128 |
+
.admin-price-item { font-size: 0.85rem; color: #333; display: block; margin-bottom: 3px;}
|
| 1129 |
+
.subcategory-list { list-style-type: none; padding-left: 20px; margin-top: 10px; }
|
| 1130 |
+
.subcategory-item { background: #f0f0f0; padding: 5px 10px; border-radius: 4px; margin-bottom: 5px; display: flex; justify-content: space-between; align-items: center; font-size: 0.9rem; }
|
| 1131 |
</style>
|
| 1132 |
</head>
|
| 1133 |
<body>
|
|
|
|
| 1165 |
<div class="section">
|
| 1166 |
<h2><i class="fas fa-tags"></i> Управление категориями</h2>
|
| 1167 |
<details>
|
| 1168 |
+
<summary><i class="fas fa-plus-circle"></i> Добавить новую основную категорию</summary>
|
| 1169 |
<div class="form-content">
|
| 1170 |
<form method="POST">
|
| 1171 |
<input type="hidden" name="action" value="add_category">
|
|
|
|
| 1180 |
{% if categories %}
|
| 1181 |
<div class="item-list">
|
| 1182 |
{% for category in categories %}
|
| 1183 |
+
<div class="item">
|
| 1184 |
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
| 1185 |
+
<strong>{{ category.name }}</strong>
|
| 1186 |
+
<form method="POST" style="margin: 0;" onsubmit="return confirm('Вы уверены, что хотите удалить категорию \'{{ category.name }}\' и все ее подкатегории?');">
|
| 1187 |
+
<input type="hidden" name="action" value="delete_category">
|
| 1188 |
+
<input type="hidden" name="category_name" value="{{ category.name }}">
|
| 1189 |
+
<button type="submit" class="delete-button" style="padding: 5px 10px; font-size: 0.8rem; margin: 0;"><i class="fas fa-trash-alt"></i></button>
|
| 1190 |
+
</form>
|
| 1191 |
+
</div>
|
| 1192 |
+
<hr style="border: 0; border-top: 1px solid #f0f0f0; margin: 10px 0;">
|
| 1193 |
+
<h4>Подкатегории:</h4>
|
| 1194 |
+
{% if category.subcategories %}
|
| 1195 |
+
<ul class="subcategory-list">
|
| 1196 |
+
{% for sub in category.subcategories %}
|
| 1197 |
+
<li class="subcategory-item">
|
| 1198 |
+
<span>{{ sub }}</span>
|
| 1199 |
+
<form method="POST" style="margin:0;" onsubmit="return confirm('Удалить подкатегорию \'{{ sub }}\'?');">
|
| 1200 |
+
<input type="hidden" name="action" value="delete_subcategory">
|
| 1201 |
+
<input type="hidden" name="parent_category" value="{{ category.name }}">
|
| 1202 |
+
<input type="hidden" name="subcategory_name" value="{{ sub }}">
|
| 1203 |
+
<button type="submit" class="delete-button" style="padding: 3px 8px; font-size:0.7rem; margin:0;"><i class="fas fa-times"></i></button>
|
| 1204 |
+
</form>
|
| 1205 |
+
</li>
|
| 1206 |
+
{% endfor %}
|
| 1207 |
+
</ul>
|
| 1208 |
+
{% else %}
|
| 1209 |
+
<p style="font-size: 0.9rem; color: #999;">Подкатегорий нет.</p>
|
| 1210 |
+
{% endif %}
|
| 1211 |
+
<form method="POST" style="margin-top: 15px; display: flex; gap: 10px; align-items: center;">
|
| 1212 |
+
<input type="hidden" name="action" value="add_subcategory">
|
| 1213 |
+
<input type="hidden" name="parent_category" value="{{ category.name }}">
|
| 1214 |
+
<input type="text" name="subcategory_name" placeholder="Новая подкатегория" required style="margin:0; flex-grow: 1;">
|
| 1215 |
+
<button type="submit" class="add-button" style="margin:0; padding: 8px 12px;"><i class="fas fa-plus"></i></button>
|
| 1216 |
+
</form>
|
| 1217 |
</div>
|
| 1218 |
{% endfor %}
|
| 1219 |
</div>
|
|
|
|
| 1257 |
|
| 1258 |
<label for="add_description" style="margin-top: 15px;">Описание:</label>
|
| 1259 |
<textarea id="add_description" name="description" rows="4"></textarea>
|
| 1260 |
+
|
| 1261 |
<label for="add_category">Категория:</label>
|
| 1262 |
+
<select id="add_category" name="category" onchange="updateSubcategoryDropdown('add', this.value)">
|
| 1263 |
<option value="Без категории">Без категории</option>
|
| 1264 |
{% for category in categories %}
|
| 1265 |
+
<option value="{{ category.name }}">{{ category.name }}</option>
|
| 1266 |
{% endfor %}
|
| 1267 |
</select>
|
| 1268 |
+
|
| 1269 |
+
<label for="add_subcategory">Подкатегория:</label>
|
| 1270 |
+
<select id="add_subcategory" name="subcategory">
|
| 1271 |
+
<option value="none">-- Сначала выберите категорию --</option>
|
| 1272 |
+
</select>
|
| 1273 |
+
|
| 1274 |
<label for="add_photos">Фотографии (до 10 шт.):</label>
|
| 1275 |
<input type="file" id="add_photos" name="photos" accept="image/*" multiple>
|
| 1276 |
<label>Цвета/Варианты (оставьте пустым, если нет):</label>
|
|
|
|
| 1323 |
<span class="status-indicator top-product"><i class="fas fa-star"></i> Топ</span>
|
| 1324 |
{% endif %}
|
| 1325 |
</h3>
|
| 1326 |
+
<p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}{% if product.get('subcategory') %} / {{ product.get('subcategory') }}{% endif %}</p>
|
| 1327 |
<p><strong>Цены:</strong></p>
|
| 1328 |
<div class="admin-price-list">
|
| 1329 |
{% if product.get('prices') %}
|
|
|
|
| 1389 |
|
| 1390 |
<label style="margin-top: 15px;">Описание:</label>
|
| 1391 |
<textarea name="description" rows="4">{{ product.get('description', '') }}</textarea>
|
| 1392 |
+
|
| 1393 |
<label>Категория:</label>
|
| 1394 |
+
<select name="category" onchange="updateSubcategoryDropdown('edit_{{ loop.index0 }}', this.value, '{{ product.get('subcategory') }}')">
|
| 1395 |
<option value="Без категории" {% if product.get('category', 'Без категории') == 'Без категории' %}selected{% endif %}>Без категории</option>
|
| 1396 |
{% for category in categories %}
|
| 1397 |
+
<option value="{{ category.name }}" {% if product.get('category') == category.name %}selected{% endif %}>{{ category.name }}</option>
|
| 1398 |
{% endfor %}
|
| 1399 |
</select>
|
| 1400 |
+
|
| 1401 |
+
<label>Подкатегория:</label>
|
| 1402 |
+
<select id="edit_{{ loop.index0 }}_subcategory" name="subcategory">
|
| 1403 |
+
<option value="none">-- Сначала выберите категорию --</option>
|
| 1404 |
+
</select>
|
| 1405 |
+
|
| 1406 |
<label>Заменить фотографии (выберите новые файлы, до 10 шт.):</label>
|
| 1407 |
<input type="file" name="photos" accept="image/*" multiple>
|
| 1408 |
{% if product.get('photos') %}
|
|
|
|
| 1457 |
</div>
|
| 1458 |
|
| 1459 |
<script>
|
| 1460 |
+
const categoriesData = {{ categories|tojson }};
|
| 1461 |
+
|
| 1462 |
function toggleEditForm(formId) {
|
| 1463 |
const formContainer = document.getElementById(formId);
|
| 1464 |
if (formContainer) {
|
| 1465 |
+
const isDisplayed = formContainer.style.display === 'block';
|
| 1466 |
+
formContainer.style.display = isDisplayed ? 'none' : 'block';
|
| 1467 |
+
if (!isDisplayed) {
|
| 1468 |
+
const catSelect = formContainer.querySelector('select[name="category"]');
|
| 1469 |
+
if (catSelect) {
|
| 1470 |
+
const currentProductSub = catSelect.getAttribute('data-current-subcategory');
|
| 1471 |
+
updateSubcategoryDropdown(catSelect.onchange.toString().match(/'(edit_[\d]+)'/)[1], catSelect.value, currentProductSub);
|
| 1472 |
+
}
|
| 1473 |
+
}
|
| 1474 |
+
}
|
| 1475 |
+
}
|
| 1476 |
+
|
| 1477 |
+
function updateSubcategoryDropdown(formIdPrefix, selectedCategory, currentSubcategory = null) {
|
| 1478 |
+
const subcatSelect = document.getElementById(`${formIdPrefix}_subcategory`);
|
| 1479 |
+
if (!subcatSelect) return;
|
| 1480 |
+
|
| 1481 |
+
subcatSelect.innerHTML = '';
|
| 1482 |
+
|
| 1483 |
+
const categoryData = categoriesData.find(c => c.name === selectedCategory);
|
| 1484 |
+
|
| 1485 |
+
if (categoryData && categoryData.subcategories && categoryData.subcategories.length > 0) {
|
| 1486 |
+
subcatSelect.disabled = false;
|
| 1487 |
+
let optionsHtml = '<option value="none">-- Выберите подкатегорию --</option>';
|
| 1488 |
+
categoryData.subcategories.forEach(sub => {
|
| 1489 |
+
const isSelected = sub === currentSubcategory ? 'selected' : '';
|
| 1490 |
+
optionsHtml += `<option value="${sub}" ${isSelected}>${sub}</option>`;
|
| 1491 |
+
});
|
| 1492 |
+
subcatSelect.innerHTML = optionsHtml;
|
| 1493 |
+
} else {
|
| 1494 |
+
subcatSelect.innerHTML = '<option value="none">-- Нет подкатегорий --</option>';
|
| 1495 |
+
subcatSelect.disabled = true;
|
| 1496 |
}
|
| 1497 |
}
|
| 1498 |
|
|
|
|
| 1518 |
if (group) {
|
| 1519 |
const container = group.parentNode;
|
| 1520 |
group.remove();
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1521 |
} else {
|
| 1522 |
console.warn("Could not find parent .color-input-group for remove button");
|
| 1523 |
}
|
|
|
|
| 1547 |
const group = button.closest('.price-input-group');
|
| 1548 |
if (group) {
|
| 1549 |
const container = group.parentNode;
|
|
|
|
| 1550 |
if (container.children.length <= 1) {
|
| 1551 |
alert("Должен быть указан хотя бы один вариант цены.");
|
| 1552 |
return;
|
|
|
|
| 1557 |
}
|
| 1558 |
}
|
| 1559 |
|
|
|
|
| 1560 |
document.addEventListener('DOMContentLoaded', () => {
|
| 1561 |
const addPriceInputsDiv = document.getElementById('add-price-inputs');
|
| 1562 |
if(addPriceInputsDiv && addPriceInputsDiv.children.length === 0) {
|
| 1563 |
addPriceInput('add-price-inputs');
|
| 1564 |
}
|
|
|
|
| 1565 |
|
| 1566 |
+
document.querySelectorAll('.edit-form-container').forEach((formContainer, index) => {
|
| 1567 |
+
const catSelect = formContainer.querySelector('select[name="category"]');
|
| 1568 |
+
const productSubcategory = "{{ products[" + index + "].get('subcategory', '') }}";
|
| 1569 |
+
if (catSelect) {
|
| 1570 |
+
const formIdPrefix = `edit_${index}`;
|
| 1571 |
+
updateSubcategoryDropdown(formIdPrefix, catSelect.value, productSubcategory);
|
| 1572 |
+
}
|
| 1573 |
+
});
|
| 1574 |
+
});
|
| 1575 |
</script>
|
| 1576 |
</body>
|
| 1577 |
</html>
|
|
|
|
| 1582 |
def catalog():
|
| 1583 |
data = load_data()
|
| 1584 |
all_products = data.get('products', [])
|
| 1585 |
+
categories = sorted(data.get('categories', []), key=lambda x: x['name'])
|
| 1586 |
|
| 1587 |
products_in_stock = [p for p in all_products if p.get('in_stock', True)]
|
| 1588 |
products_sorted = sorted(products_in_stock, key=lambda p: (not p.get('is_top', False), p.get('name', '').lower()))
|
|
|
|
| 1629 |
total_price = 0
|
| 1630 |
processed_cart = []
|
| 1631 |
for item in cart_items:
|
|
|
|
| 1632 |
if not all(k in item for k in ('name', 'price_value', 'quantity', 'price_type')):
|
| 1633 |
logging.error(f"Invalid cart item structure received: {item}")
|
| 1634 |
return jsonify({"error": "Неверный формат товара в корзине."}), 400
|
|
|
|
| 1715 |
try:
|
| 1716 |
if action == 'add_category':
|
| 1717 |
category_name = request.form.get('category_name', '').strip()
|
| 1718 |
+
if category_name and not any(c['name'] == category_name for c in categories):
|
| 1719 |
+
categories.append({'name': category_name, 'subcategories': []})
|
| 1720 |
+
data['categories'] = sorted(categories, key=lambda x: x['name'])
|
| 1721 |
save_data(data)
|
| 1722 |
logging.info(f"Category '{category_name}' added.")
|
| 1723 |
flash(f"Категория '{category_name}' успешно добавлена.", 'success')
|
|
|
|
| 1728 |
logging.warning(f"Category '{category_name}' already exists.")
|
| 1729 |
flash(f"Категория '{category_name}' уже существует.", 'error')
|
| 1730 |
|
| 1731 |
+
elif action == 'add_subcategory':
|
| 1732 |
+
parent_category_name = request.form.get('parent_category')
|
| 1733 |
+
subcategory_name = request.form.get('subcategory_name', '').strip()
|
| 1734 |
+
parent_cat = next((c for c in categories if c['name'] == parent_category_name), None)
|
| 1735 |
+
|
| 1736 |
+
if parent_cat and subcategory_name and subcategory_name not in parent_cat['subcategories']:
|
| 1737 |
+
parent_cat['subcategories'].append(subcategory_name)
|
| 1738 |
+
parent_cat['subcategories'].sort()
|
| 1739 |
+
save_data(data)
|
| 1740 |
+
flash(f"Подкатегория '{subcategory_name}' добавлена в '{parent_category_name}'.", 'success')
|
| 1741 |
+
elif not parent_cat:
|
| 1742 |
+
flash(f"Родительская категория '{parent_category_name}' не найдена.", 'error')
|
| 1743 |
+
elif not subcategory_name:
|
| 1744 |
+
flash("Название подкатегории не может быть пустым.", 'error')
|
| 1745 |
+
else:
|
| 1746 |
+
flash(f"Подкатегория '{subcategory_name}' уже существует в '{parent_category_name}'.", 'error')
|
| 1747 |
+
|
| 1748 |
elif action == 'delete_category':
|
| 1749 |
category_to_delete = request.form.get('category_name')
|
| 1750 |
+
original_len = len(categories)
|
| 1751 |
+
categories = [c for c in categories if c['name'] != category_to_delete]
|
| 1752 |
+
if len(categories) < original_len:
|
| 1753 |
updated_count = 0
|
| 1754 |
for product in products:
|
| 1755 |
if product.get('category') == category_to_delete:
|
| 1756 |
product['category'] = 'Без категории'
|
| 1757 |
+
product['subcategory'] = None
|
| 1758 |
updated_count += 1
|
| 1759 |
data['categories'] = categories
|
| 1760 |
data['products'] = products
|
|
|
|
| 1762 |
logging.info(f"Category '{category_to_delete}' deleted. Updated products: {updated_count}.")
|
| 1763 |
flash(f"Категория '{category_to_delete}' удалена. {updated_count} товаров обновлено.", 'success')
|
| 1764 |
else:
|
|
|
|
| 1765 |
flash(f"Не удалось удалить категорию '{category_to_delete}'.", 'error')
|
| 1766 |
|
| 1767 |
+
elif action == 'delete_subcategory':
|
| 1768 |
+
parent_category_name = request.form.get('parent_category')
|
| 1769 |
+
subcategory_to_delete = request.form.get('subcategory_name')
|
| 1770 |
+
parent_cat = next((c for c in categories if c['name'] == parent_category_name), None)
|
| 1771 |
+
|
| 1772 |
+
if parent_cat and subcategory_to_delete in parent_cat['subcategories']:
|
| 1773 |
+
parent_cat['subcategories'].remove(subcategory_to_delete)
|
| 1774 |
+
updated_count = 0
|
| 1775 |
+
for product in products:
|
| 1776 |
+
if product.get('category') == parent_category_name and product.get('subcategory') == subcategory_to_delete:
|
| 1777 |
+
product['subcategory'] = None
|
| 1778 |
+
updated_count += 1
|
| 1779 |
+
save_data(data)
|
| 1780 |
+
flash(f"Подкатегория '{subcategory_to_delete}' удалена. {updated_count} товаров обновлено.", 'success')
|
| 1781 |
+
else:
|
| 1782 |
+
flash(f"Не удалось удалить подкатегорию '{subcategory_to_delete}'.", 'error')
|
| 1783 |
+
|
| 1784 |
elif action == 'add_product':
|
| 1785 |
name = request.form.get('name', '').strip()
|
| 1786 |
description = request.form.get('description', '').strip()
|
| 1787 |
category = request.form.get('category')
|
| 1788 |
+
subcategory = request.form.get('subcategory')
|
| 1789 |
photos_files = request.files.getlist('photos')
|
| 1790 |
colors = [c.strip() for c in request.form.getlist('colors') if c.strip()]
|
| 1791 |
in_stock = 'in_stock' in request.form
|
|
|
|
| 1799 |
type_str = type_str.strip()
|
| 1800 |
value_str = value_str.strip().replace(',', '.')
|
| 1801 |
if not type_str or not value_str:
|
| 1802 |
+
continue
|
| 1803 |
|
| 1804 |
try:
|
| 1805 |
price_value = round(float(value_str), 2)
|
|
|
|
| 1809 |
except ValueError:
|
| 1810 |
logging.warning(f"Skipping invalid price value '{value_str}' for type '{type_str}' during add product.")
|
| 1811 |
|
|
|
|
| 1812 |
if not name:
|
| 1813 |
flash("Название товара обязательно.", 'error')
|
| 1814 |
return redirect(url_for('admin'))
|
|
|
|
| 1873 |
|
| 1874 |
new_product = {
|
| 1875 |
'name': name, 'prices': prices, 'description': description,
|
| 1876 |
+
'category': category if any(c['name'] == category for c in categories) else 'Без категории',
|
| 1877 |
+
'subcategory': subcategory if subcategory and subcategory != 'none' else None,
|
| 1878 |
'photos': photos_list, 'colors': colors,
|
| 1879 |
'in_stock': in_stock, 'is_top': is_top
|
| 1880 |
}
|
|
|
|
| 1905 |
product_to_edit['name'] = request.form.get('name', product_to_edit['name']).strip()
|
| 1906 |
product_to_edit['description'] = request.form.get('description', product_to_edit.get('description', '')).strip()
|
| 1907 |
category = request.form.get('category')
|
| 1908 |
+
subcategory = request.form.get('subcategory')
|
| 1909 |
+
product_to_edit['category'] = category if any(c['name'] == category for c in categories) else 'Без категории'
|
| 1910 |
+
product_to_edit['subcategory'] = subcategory if subcategory and subcategory != 'none' else None
|
| 1911 |
product_to_edit['colors'] = [c.strip() for c in request.form.getlist('colors') if c.strip()]
|
| 1912 |
product_to_edit['in_stock'] = 'in_stock' in request.form
|
| 1913 |
product_to_edit['is_top'] = 'is_top' in request.form
|
|
|
|
| 1920 |
type_str = type_str.strip()
|
| 1921 |
value_str = value_str.strip().replace(',', '.')
|
| 1922 |
if not type_str or not value_str:
|
| 1923 |
+
continue
|
| 1924 |
|
| 1925 |
try:
|
| 1926 |
price_value = round(float(value_str), 2)
|
|
|
|
| 2070 |
|
| 2071 |
current_data = load_data()
|
| 2072 |
display_products = sorted(current_data.get('products', []), key=lambda p: p.get('name', '').lower())
|
| 2073 |
+
display_categories = sorted(current_data.get('categories', []), key=lambda x: x.get('name', ''))
|
| 2074 |
|
| 2075 |
return render_template_string(
|
| 2076 |
ADMIN_TEMPLATE,
|
|
|
|
| 2122 |
port = int(os.environ.get('PORT', 7860))
|
| 2123 |
logging.info(f"Starting Flask app on host 0.0.0.0 and port {port}")
|
| 2124 |
app.run(debug=False, host='0.0.0.0', port=port)
|
|
|