Spaces:
Paused
Paused
Update app.py
Browse files
app.py
CHANGED
|
@@ -28,7 +28,6 @@ HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
|
|
| 28 |
|
| 29 |
STORE_ADDRESSES = [
|
| 30 |
"Город Алматы , Алатау 1 , блок 4 , бутик 112",
|
| 31 |
-
|
| 32 |
]
|
| 33 |
|
| 34 |
|
|
@@ -55,8 +54,8 @@ STATUS_MAPS = {
|
|
| 55 |
|
| 56 |
translations = {
|
| 57 |
'ru': {
|
| 58 |
-
'page_title': "
|
| 59 |
-
'header_title': "
|
| 60 |
'our_addresses': "Наши адреса в г. Алматы",
|
| 61 |
'search_placeholder': "Поиск по названию или описанию...",
|
| 62 |
'all_filter': "Все",
|
|
@@ -68,7 +67,8 @@ translations = {
|
|
| 68 |
'product_load_error': "Не удалось загрузить информацию о товаре.",
|
| 69 |
'specify_details': "Укажите детали",
|
| 70 |
'variant_label': "Вариант:",
|
| 71 |
-
'
|
|
|
|
| 72 |
'confirm_add_to_cart': "Добавить в корзину",
|
| 73 |
'your_cart': "Ваша корзина",
|
| 74 |
'cart_is_empty': "Ваша корзина пуста.",
|
|
@@ -76,6 +76,7 @@ translations = {
|
|
| 76 |
'clear_cart_button': "Очистить",
|
| 77 |
'formulate_order_button': "Оформить заказ",
|
| 78 |
'cart_item_variant': "Вариант",
|
|
|
|
| 79 |
'remove_item_title': "Удалить товар",
|
| 80 |
'clear_cart_confirm': "Вы уверены, что хотите очистить корзину?",
|
| 81 |
'cart_is_empty_alert': "Корзина пуста!",
|
|
@@ -83,10 +84,12 @@ translations = {
|
|
| 83 |
'add_to_cart_notification': "добавлен в корзину!",
|
| 84 |
'no_products_found': "По вашему запросу товары не найдены.",
|
| 85 |
'category': "Категория",
|
| 86 |
-
'
|
|
|
|
|
|
|
| 87 |
'no_category': "Без категории",
|
| 88 |
-
'no_brand': "Без бренда",
|
| 89 |
'available_variants': "Доступные варианты:",
|
|
|
|
| 90 |
'description': "Описание:",
|
| 91 |
'no_description': "Описание отсутствует.",
|
| 92 |
'order_page_title': "Заказ №",
|
|
@@ -100,14 +103,14 @@ translations = {
|
|
| 100 |
'return_to_catalog': "Вернуться в каталог",
|
| 101 |
'order_not_found_error': "Ошибка",
|
| 102 |
'order_not_found_message': "Заказ с таким ID не найден.",
|
| 103 |
-
'whatsapp_confirm_message_1': "Здравствуйте! Хочу подтвердить свой заказ
|
| 104 |
'whatsapp_confirm_message_2': "Номер заказа:",
|
| 105 |
'whatsapp_confirm_message_3': "Ссылка на заказ:",
|
| 106 |
'whatsapp_confirm_message_4': "Пожалуйста, свяжитесь со мной для уточнения деталей.",
|
| 107 |
},
|
| 108 |
'kz': {
|
| 109 |
-
'page_title': "
|
| 110 |
-
'header_title': "
|
| 111 |
'our_addresses': "Алматы қаласындағы мекенжайларымыз",
|
| 112 |
'search_placeholder': "Аты немесе сипаттамасы бойынша іздеу...",
|
| 113 |
'all_filter': "Барлығы",
|
|
@@ -119,7 +122,8 @@ translations = {
|
|
| 119 |
'product_load_error': "Тауар туралы ақпаратты жүктеу мүмкін болмады.",
|
| 120 |
'specify_details': "Мәліметтерді көрсетіңіз",
|
| 121 |
'variant_label': "Нұсқа:",
|
| 122 |
-
'
|
|
|
|
| 123 |
'confirm_add_to_cart': "Себетке қосу",
|
| 124 |
'your_cart': "Сіздің себетіңіз",
|
| 125 |
'cart_is_empty': "Сіздің себетіңіз бос.",
|
|
@@ -127,6 +131,7 @@ translations = {
|
|
| 127 |
'clear_cart_button': "Тазарту",
|
| 128 |
'formulate_order_button': "Тапсырыс беру",
|
| 129 |
'cart_item_variant': "Нұсқа",
|
|
|
|
| 130 |
'remove_item_title': "Тауарды жою",
|
| 131 |
'clear_cart_confirm': "Себетті тазалағыңыз келетініне сенімдісіз бе?",
|
| 132 |
'cart_is_empty_alert': "Себет бос!",
|
|
@@ -134,10 +139,12 @@ translations = {
|
|
| 134 |
'add_to_cart_notification': "себетке қосылды!",
|
| 135 |
'no_products_found': "Сіздің сұранысыңыз бойынша тауарлар табылмады.",
|
| 136 |
'category': "Санат",
|
| 137 |
-
'
|
|
|
|
|
|
|
| 138 |
'no_category': "Санатсыз",
|
| 139 |
-
'no_brand': "Брендсіз",
|
| 140 |
'available_variants': "Қолжетімді нұсқалар:",
|
|
|
|
| 141 |
'description': "Сипаттама:",
|
| 142 |
'no_description': "Сипаттама жоқ.",
|
| 143 |
'order_page_title': "Тапсырыс №",
|
|
@@ -151,7 +158,7 @@ translations = {
|
|
| 151 |
'return_to_catalog': "Каталогқа оралу",
|
| 152 |
'order_not_found_error': "Қате",
|
| 153 |
'order_not_found_message': "Мұндай ID-мен тапсырыс табылмады.",
|
| 154 |
-
'whatsapp_confirm_message_1': "Сәлеметсіз бе!
|
| 155 |
'whatsapp_confirm_message_2': "Тапсырыс нөмірі:",
|
| 156 |
'whatsapp_confirm_message_3': "Тапсырысқа сілтеме:",
|
| 157 |
'whatsapp_confirm_message_4': "Мәліметтерді нақтылау үшін менімен хабарласуыңызды сұраймын.",
|
|
@@ -201,7 +208,7 @@ def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWN
|
|
| 201 |
try:
|
| 202 |
if file_name == DATA_FILE:
|
| 203 |
with open(file_name, 'w', encoding='utf-8') as f:
|
| 204 |
-
json.dump({'products': [], 'categories': [], '
|
| 205 |
logging.info(f"Created empty local file {file_name} because it was not found on HF.")
|
| 206 |
except Exception as create_e:
|
| 207 |
logging.error(f"Failed to create empty local file {file_name}: {create_e}")
|
|
@@ -265,7 +272,7 @@ def periodic_backup():
|
|
| 265 |
|
| 266 |
|
| 267 |
def load_data():
|
| 268 |
-
default_data = {'products': [], 'categories': [], '
|
| 269 |
try:
|
| 270 |
with open(DATA_FILE, 'r', encoding='utf-8') as file:
|
| 271 |
data = json.load(file)
|
|
@@ -275,8 +282,8 @@ def load_data():
|
|
| 275 |
raise FileNotFoundError
|
| 276 |
if 'products' not in data: data['products'] = []
|
| 277 |
if 'categories' not in data: data['categories'] = []
|
| 278 |
-
if 'brands' not in data: data['brands'] = []
|
| 279 |
if 'orders' not in data: data['orders'] = {}
|
|
|
|
| 280 |
return data
|
| 281 |
except FileNotFoundError:
|
| 282 |
logging.warning(f"Local file {DATA_FILE} not found. Attempting download from HF.")
|
|
@@ -293,8 +300,8 @@ def load_data():
|
|
| 293 |
return default_data
|
| 294 |
if 'products' not in data: data['products'] = []
|
| 295 |
if 'categories' not in data: data['categories'] = []
|
| 296 |
-
if 'brands' not in data: data['brands'] = []
|
| 297 |
if 'orders' not in data: data['orders'] = {}
|
|
|
|
| 298 |
return data
|
| 299 |
except FileNotFoundError:
|
| 300 |
logging.error(f"File {DATA_FILE} still not found even after download reported success. Using default.")
|
|
@@ -323,8 +330,8 @@ def save_data(data):
|
|
| 323 |
return
|
| 324 |
if 'products' not in data: data['products'] = []
|
| 325 |
if 'categories' not in data: data['categories'] = []
|
| 326 |
-
if 'brands' not in data: data['brands'] = []
|
| 327 |
if 'orders' not in data: data['orders'] = {}
|
|
|
|
| 328 |
|
| 329 |
with open(DATA_FILE, 'w', encoding='utf-8') as file:
|
| 330 |
json.dump(data, file, ensure_ascii=False, indent=4)
|
|
@@ -349,29 +356,29 @@ CATALOG_TEMPLATE = '''
|
|
| 349 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.css">
|
| 350 |
<style>
|
| 351 |
:root {
|
| 352 |
-
--primary-color: #
|
| 353 |
-
--primary-dark: #
|
| 354 |
-
--surface-color: #
|
| 355 |
-
--background-color: #
|
| 356 |
--text-color: #F5F5F5;
|
| 357 |
-
--text-color-muted: #
|
| 358 |
-
--border-color: #
|
| 359 |
--success-color: #28a745;
|
| 360 |
}
|
| 361 |
* { margin: 0; padding: 0; box-sizing: border-box; }
|
| 362 |
-
body {
|
| 363 |
-
font-family: 'Georgia', serif;
|
| 364 |
-
background: var(--background-color);
|
| 365 |
-
color: var(--text-color);
|
| 366 |
-
line-height: 1.6;
|
| 367 |
-
transition: background-color 0.3s;
|
| 368 |
}
|
| 369 |
.container { max-width: 100%; margin: 0 auto; padding: 0; }
|
| 370 |
.content-area { padding: 20px; }
|
| 371 |
|
| 372 |
-
.header { display: flex; justify-content: space-between; align-items: center; padding: 15px 20px; background: rgba(
|
| 373 |
.logo-title-container { display: flex; align-items: center; gap: 15px; }
|
| 374 |
-
.logo-title-container img { height: 45px; width: 45px; border-radius: 50%; object-fit: cover;
|
| 375 |
.header h1 { font-family: 'Cormorant Garamond', serif; font-size: 1.8rem; font-weight: 700; color: var(--text-color); }
|
| 376 |
|
| 377 |
.lang-switcher { display: flex; gap: 5px; background-color: var(--surface-color); padding: 5px; border-radius: 50px; }
|
|
@@ -384,20 +391,20 @@ CATALOG_TEMPLATE = '''
|
|
| 384 |
|
| 385 |
.search-container { padding: 0 20px 20px; }
|
| 386 |
#search-input { width: 100%; padding: 12px 20px; font-size: 1rem; border: 1px solid var(--border-color); border-radius: 50px; outline: none; transition: all 0.3s; background-color: var(--surface-color); color: var(--text-color); }
|
| 387 |
-
#search-input:focus { border-color: var(--primary-color); box-shadow: 0 0 0 4px rgba(
|
| 388 |
|
| 389 |
.filters-wrapper { margin: 0 20px 20px; display: flex; flex-direction: column; gap: 15px; }
|
| 390 |
.filters-container { display: flex; overflow-x: auto; gap: 10px; padding-bottom: 10px; scrollbar-width: none; -ms-overflow-style: none; }
|
| 391 |
.filters-container::-webkit-scrollbar { display: none; }
|
| 392 |
.filter-label { font-size: 0.9rem; color: var(--text-color-muted); margin-left: 5px; }
|
| 393 |
-
.
|
| 394 |
-
.
|
| 395 |
|
| 396 |
.products-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 15px; padding: 0 20px 120px; }
|
| 397 |
@media (min-width: 600px) { .products-grid { grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 20px; } }
|
| 398 |
|
| 399 |
.product { background: var(--surface-color); border-radius: 16px; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4); transition: all 0.3s ease; overflow: hidden; display: flex; flex-direction: column; height: 100%; position: relative; border: 1px solid var(--border-color); }
|
| 400 |
-
.product:hover { transform: translateY(-5px); box-shadow: 0 8px 25px rgba(
|
| 401 |
|
| 402 |
.product-image { width: 100%; aspect-ratio: 1 / 1; background-color: #000; display: flex; justify-content: center; align-items: center; padding: 10px; }
|
| 403 |
.product-image img { max-width: 100%; max-height: 100%; object-fit: contain; transition: transform 0.3s ease; }
|
|
@@ -410,10 +417,10 @@ CATALOG_TEMPLATE = '''
|
|
| 410 |
.product-actions { padding: 0 15px 15px; }
|
| 411 |
|
| 412 |
.product-button { display: inline-flex; align-items: center; justify-content: center; width: 100%; padding: 10px; border: none; border-radius: 50px; background-color: var(--primary-color); color: #fff; font-size: 0.9rem; font-weight: 500; cursor: pointer; transition: all 0.3s ease; text-decoration: none; text-transform: uppercase; letter-spacing: 0.5px; }
|
| 413 |
-
.product-button:hover { background-color: var(--primary-dark); box-shadow: 0 4px 10px rgba(
|
| 414 |
.product-button i { margin-right: 8px; }
|
| 415 |
|
| 416 |
-
.fab { position: fixed; background-color: var(--primary-color); color: #fff; border: none; border-radius: 50%; width: 55px; height: 55px; font-size: 1.5rem; cursor: pointer; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 15px rgba(
|
| 417 |
.fab:hover { transform: scale(1.1); }
|
| 418 |
#cart-button { bottom: 20px; right: 20px; display: none; }
|
| 419 |
#cart-button span { position: absolute; top: -2px; right: -2px; background-color: #dc3545; color: white; border-radius: 50%; padding: 3px 7px; font-size: 0.75rem; font-weight: bold; min-width: 22px; text-align: center; }
|
|
@@ -436,7 +443,7 @@ CATALOG_TEMPLATE = '''
|
|
| 436 |
.cart-item-remove { background:none; border:none; color:#FF453A; cursor:pointer; font-size: 1.5rem; line-height: 1; transition: color 0.2s; }
|
| 437 |
.cart-item-remove:hover { color: #ff0000; }
|
| 438 |
|
| 439 |
-
.quantity-input, .variant-select { width: 100%; padding: 12px; border: 1px solid var(--border-color); border-radius: 8px; font-size: 1rem; margin-top: 8px; margin-bottom: 20px; box-sizing: border-box; background-color: var(--background-color); color: var(--text-color); }
|
| 440 |
|
| 441 |
.cart-summary { margin-top: 25px; text-align: right; border-top: 1px solid var(--border-color); padding-top: 20px; }
|
| 442 |
.cart-summary strong { font-size: 1.4rem; color: var(--primary-color); }
|
|
@@ -448,22 +455,16 @@ CATALOG_TEMPLATE = '''
|
|
| 448 |
.notification { position: fixed; bottom: 80px; left: 50%; transform: translateX(-50%); background-color: var(--primary-color); color: #fff; padding: 12px 25px; border-radius: 50px; box-shadow: 0 4px 15px rgba(0,0,0,0.5); z-index: 1002; opacity: 0; transition: all 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94); font-size: 0.95rem; font-weight: 500; }
|
| 449 |
.notification.show { opacity: 1; bottom: 90px; }
|
| 450 |
.no-results-message { grid-column: 1 / -1; text-align: center; padding: 50px; font-size: 1.2rem; color: var(--text-color-muted); }
|
| 451 |
-
.top-product-indicator { position: absolute; top: 10px; right: 10px; background: linear-gradient(135deg, #
|
| 452 |
|
| 453 |
#whatsapp-fab { bottom: 20px; left: 20px; background-color: #25D366; color: white; }
|
| 454 |
-
#whatsapp-modal { display: none; position: fixed; bottom: 85px; left: 20px; background-color: var(--surface-color); border-radius: 12px; box-shadow: 0 5px 20px rgba(0,0,0,0.6); z-index: 1001; overflow: hidden; border: 1px solid var(--border-color); animation: fadeIn 0.3s; }
|
| 455 |
-
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
|
| 456 |
-
#whatsapp-modal a { display: flex; align-items: center; gap: 15px; padding: 15px 20px; text-decoration: none; color: var(--text-color); transition: background-color 0.2s; }
|
| 457 |
-
#whatsapp-modal a:first-child { border-bottom: 1px solid var(--border-color); }
|
| 458 |
-
#whatsapp-modal a:hover { background-color: rgba(255,255,255,0.05); }
|
| 459 |
-
#whatsapp-modal i { color: #25D366; font-size: 1.6rem; }
|
| 460 |
</style>
|
| 461 |
</head>
|
| 462 |
<body>
|
| 463 |
<div class="container">
|
| 464 |
<header class="header">
|
| 465 |
<div class="logo-title-container">
|
| 466 |
-
<img src="https://huggingface.co/spaces/
|
| 467 |
<h1>{{ _['header_title'] }}</h1>
|
| 468 |
</div>
|
| 469 |
<div class="lang-switcher">
|
|
@@ -486,25 +487,22 @@ CATALOG_TEMPLATE = '''
|
|
| 486 |
|
| 487 |
<div class="filters-wrapper">
|
| 488 |
<div>
|
| 489 |
-
<span class="filter-label">{{ _['
|
| 490 |
<div class="filters-container">
|
| 491 |
-
<button class="
|
| 492 |
-
|
| 493 |
-
<button class="
|
| 494 |
-
{% endfor %}
|
| 495 |
</div>
|
| 496 |
</div>
|
| 497 |
-
{% if brands %}
|
| 498 |
<div>
|
| 499 |
-
<span class="filter-label">{{ _['
|
| 500 |
<div class="filters-container">
|
| 501 |
-
<button class="
|
| 502 |
-
{% for
|
| 503 |
-
<button class="
|
| 504 |
{% endfor %}
|
| 505 |
</div>
|
| 506 |
</div>
|
| 507 |
-
{% endif %}
|
| 508 |
</div>
|
| 509 |
|
| 510 |
|
|
@@ -514,7 +512,7 @@ CATALOG_TEMPLATE = '''
|
|
| 514 |
data-name="{{ product['name']|lower }}"
|
| 515 |
data-description="{{ product.get('description', '')|lower }}"
|
| 516 |
data-category="{{ product.get('category', _['no_category']) }}"
|
| 517 |
-
data-
|
| 518 |
{% if product.get('is_top', False) %}
|
| 519 |
<span class="top-product-indicator"><i class="fas fa-star fa-xs"></i> {{ _['top_product'] }}</span>
|
| 520 |
{% endif %}
|
|
@@ -561,11 +559,17 @@ CATALOG_TEMPLATE = '''
|
|
| 561 |
<div class="modal-content">
|
| 562 |
<span class="close" onclick="closeModal('quantityModal')" aria-label="Закрыть">×</span>
|
| 563 |
<h2>{{ _['specify_details'] }}</h2>
|
|
|
|
| 564 |
<label for="variantSelect">{{ _['variant_label'] }}</label>
|
| 565 |
<select id="variantSelect" class="variant-select"></select>
|
| 566 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 567 |
<label for="quantityInput">{{ _['quantity_label'] }}</label>
|
| 568 |
-
<input type="number" id="quantityInput" class="quantity-input" min="1" value="1">
|
| 569 |
|
| 570 |
<button class="product-button" onclick="confirmAddToCart()"><i class="fas fa-check"></i> {{ _['confirm_add_to_cart'] }}</button>
|
| 571 |
</div>
|
|
@@ -595,17 +599,7 @@ CATALOG_TEMPLATE = '''
|
|
| 595 |
<span id="cart-count">0</span>
|
| 596 |
</button>
|
| 597 |
|
| 598 |
-
<
|
| 599 |
-
<div id="whatsapp-modal">
|
| 600 |
-
<a href="https://api.whatsapp.com/send?phone=77762021169" target="_blank" rel="noopener noreferrer">
|
| 601 |
-
<i class="fab fa-whatsapp"></i>
|
| 602 |
-
<span>WhatsApp 1</span>
|
| 603 |
-
</a>
|
| 604 |
-
<a href="https://api.whatsapp.com/send?phone=77711094111" target="_blank" rel="noopener noreferrer">
|
| 605 |
-
<i class="fab fa-whatsapp"></i>
|
| 606 |
-
<span>WhatsApp 2</span>
|
| 607 |
-
</a>
|
| 608 |
-
</div>
|
| 609 |
|
| 610 |
<div id="notification-placeholder"></div>
|
| 611 |
|
|
@@ -615,7 +609,7 @@ CATALOG_TEMPLATE = '''
|
|
| 615 |
const repoId = '{{ repo_id }}';
|
| 616 |
const currencyCode = '{{ currency_code }}';
|
| 617 |
let selectedProductIndex = null;
|
| 618 |
-
let cart = JSON.parse(localStorage.getItem('
|
| 619 |
const langCode = '{{ lang_code }}';
|
| 620 |
const translations = {{ _|tojson }};
|
| 621 |
|
|
@@ -632,11 +626,6 @@ CATALOG_TEMPLATE = '''
|
|
| 632 |
document.body.style.overflow = 'auto';
|
| 633 |
}
|
| 634 |
}
|
| 635 |
-
|
| 636 |
-
function toggleWhatsAppModal() {
|
| 637 |
-
const modal = document.getElementById('whatsapp-modal');
|
| 638 |
-
modal.style.display = modal.style.display === 'block' ? 'none' : 'block';
|
| 639 |
-
}
|
| 640 |
|
| 641 |
function loadProductDetails(index) {
|
| 642 |
const modalContent = document.getElementById('modalContent');
|
|
@@ -663,19 +652,19 @@ CATALOG_TEMPLATE = '''
|
|
| 663 |
});
|
| 664 |
}
|
| 665 |
|
| 666 |
-
function
|
| 667 |
-
const select = document.getElementById(
|
| 668 |
select.innerHTML = '';
|
| 669 |
-
if (
|
| 670 |
-
|
| 671 |
-
const
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
select.appendChild(
|
| 675 |
});
|
| 676 |
-
select.style.display = 'block';
|
| 677 |
} else {
|
| 678 |
-
select.style.display = 'none';
|
| 679 |
}
|
| 680 |
}
|
| 681 |
|
|
@@ -684,8 +673,24 @@ CATALOG_TEMPLATE = '''
|
|
| 684 |
const product = products[index];
|
| 685 |
if (!product) return;
|
| 686 |
|
| 687 |
-
|
| 688 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 689 |
document.getElementById('quantityModal').style.display = 'block';
|
| 690 |
document.body.style.overflow = 'hidden';
|
| 691 |
}
|
|
@@ -693,23 +698,38 @@ CATALOG_TEMPLATE = '''
|
|
| 693 |
function confirmAddToCart() {
|
| 694 |
if (selectedProductIndex === null) return;
|
| 695 |
|
| 696 |
-
const
|
| 697 |
-
const variantIndex = document.getElementById('variantSelect').value;
|
| 698 |
const product = products[selectedProductIndex];
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
|
|
|
|
|
|
| 702 |
return;
|
| 703 |
}
|
| 704 |
|
|
|
|
| 705 |
const selectedVariant = product.variants[variantIndex];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 706 |
|
| 707 |
-
if (
|
| 708 |
-
alert("
|
| 709 |
return;
|
| 710 |
}
|
| 711 |
|
| 712 |
-
const cartItemId = `${product.id}-${selectedVariant.name}`;
|
| 713 |
const existingItem = cart.find(item => item.id === cartItemId);
|
| 714 |
|
| 715 |
if (existingItem) {
|
|
@@ -721,21 +741,22 @@ CATALOG_TEMPLATE = '''
|
|
| 721 |
name: product.name,
|
| 722 |
price: selectedVariant.price,
|
| 723 |
variantName: selectedVariant.name,
|
|
|
|
| 724 |
photo: product.photos && product.photos.length > 0 ? product.photos[0] : null,
|
| 725 |
quantity: quantity
|
| 726 |
});
|
| 727 |
}
|
| 728 |
|
| 729 |
-
localStorage.setItem('
|
| 730 |
closeModal('quantityModal');
|
| 731 |
updateCartButton();
|
| 732 |
-
showNotification(`${product.name}
|
| 733 |
}
|
| 734 |
|
| 735 |
function updateCartButton() {
|
| 736 |
const cartCountElement = document.getElementById('cart-count');
|
| 737 |
const cartButton = document.getElementById('cart-button');
|
| 738 |
-
const totalItems = cart.
|
| 739 |
|
| 740 |
if (totalItems > 0) {
|
| 741 |
cartCountElement.textContent = totalItems;
|
|
@@ -759,13 +780,18 @@ CATALOG_TEMPLATE = '''
|
|
| 759 |
total += itemTotal;
|
| 760 |
const photoUrl = item.photo ? `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${item.photo}` : 'https://via.placeholder.com/65x65.png?text=N/A';
|
| 761 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 762 |
return `
|
| 763 |
<div class="cart-item">
|
| 764 |
<img src="${photoUrl}" alt="${item.name}">
|
| 765 |
<div class="cart-item-details">
|
| 766 |
<strong>${item.name}</strong>
|
| 767 |
-
|
| 768 |
-
<p class="cart-item-price">${item.price.toFixed(2)} ${currencyCode} × ${item.quantity}</p>
|
| 769 |
</div>
|
| 770 |
<span class="cart-item-total">${itemTotal.toFixed(2)}</span>
|
| 771 |
<button class="cart-item-remove" onclick="removeFromCart('${item.id}')" title="${translations['remove_item_title']}">×</button>
|
|
@@ -779,7 +805,7 @@ CATALOG_TEMPLATE = '''
|
|
| 779 |
|
| 780 |
function removeFromCart(itemId) {
|
| 781 |
cart = cart.filter(item => item.id !== itemId);
|
| 782 |
-
localStorage.setItem('
|
| 783 |
openCartModal();
|
| 784 |
updateCartButton();
|
| 785 |
}
|
|
@@ -787,7 +813,7 @@ CATALOG_TEMPLATE = '''
|
|
| 787 |
function clearCart() {
|
| 788 |
if (confirm(translations['clear_cart_confirm'])) {
|
| 789 |
cart = [];
|
| 790 |
-
localStorage.removeItem('
|
| 791 |
openCartModal();
|
| 792 |
updateCartButton();
|
| 793 |
}
|
|
@@ -809,7 +835,7 @@ CATALOG_TEMPLATE = '''
|
|
| 809 |
.then(response => response.json())
|
| 810 |
.then(data => {
|
| 811 |
if (data.order_id) {
|
| 812 |
-
localStorage.removeItem('
|
| 813 |
cart = [];
|
| 814 |
updateCartButton();
|
| 815 |
closeModal('cartModal');
|
|
@@ -828,8 +854,7 @@ CATALOG_TEMPLATE = '''
|
|
| 828 |
function filterProducts() {
|
| 829 |
const searchTerm = document.getElementById('search-input').value.toLowerCase().trim();
|
| 830 |
const activeCategory = document.querySelector('.category-filter.active').dataset.category;
|
| 831 |
-
const
|
| 832 |
-
const activeBrand = activeBrandEl ? activeBrandEl.dataset.brand : 'all';
|
| 833 |
const grid = document.getElementById('products-grid');
|
| 834 |
let visibleProducts = 0;
|
| 835 |
|
|
@@ -840,13 +865,13 @@ CATALOG_TEMPLATE = '''
|
|
| 840 |
const name = productElement.dataset.name;
|
| 841 |
const description = productElement.dataset.description;
|
| 842 |
const category = productElement.dataset.category;
|
| 843 |
-
const
|
| 844 |
|
| 845 |
const matchesSearch = !searchTerm || name.includes(searchTerm) || description.includes(searchTerm);
|
| 846 |
const matchesCategory = activeCategory === 'all' || category === activeCategory;
|
| 847 |
-
const
|
| 848 |
|
| 849 |
-
if (matchesSearch && matchesCategory &&
|
| 850 |
productElement.style.display = 'flex';
|
| 851 |
visibleProducts++;
|
| 852 |
} else {
|
|
@@ -871,9 +896,9 @@ CATALOG_TEMPLATE = '''
|
|
| 871 |
filterProducts();
|
| 872 |
});
|
| 873 |
});
|
| 874 |
-
document.querySelectorAll('.
|
| 875 |
filter.addEventListener('click', function() {
|
| 876 |
-
document.querySelector('.
|
| 877 |
this.classList.add('active');
|
| 878 |
filterProducts();
|
| 879 |
});
|
|
@@ -903,11 +928,6 @@ CATALOG_TEMPLATE = '''
|
|
| 903 |
if (event.target.classList.contains('modal')) {
|
| 904 |
closeModal(event.target.id);
|
| 905 |
}
|
| 906 |
-
const waModal = document.getElementById('whatsapp-modal');
|
| 907 |
-
const waFab = document.getElementById('whatsapp-fab');
|
| 908 |
-
if (waModal.style.display === 'block' && !waModal.contains(event.target) && event.target !== waFab && !waFab.contains(event.target)) {
|
| 909 |
-
waModal.style.display = 'none';
|
| 910 |
-
}
|
| 911 |
});
|
| 912 |
|
| 913 |
window.addEventListener('keydown', function(event) {
|
|
@@ -951,8 +971,8 @@ PRODUCT_DETAIL_TEMPLATE = '''
|
|
| 951 |
</div>
|
| 952 |
|
| 953 |
<div style="font-size: 1rem; line-height: 1.7; padding: 0 10px;">
|
|
|
|
| 954 |
<p><strong>{{ _['category'] }}:</strong> {{ product.get('category', _['no_category']) }}</p>
|
| 955 |
-
<p><strong>{{ _['brand'] }}:</strong> {{ product.get('brand', _['no_brand']) }}</p>
|
| 956 |
{% if product.get('variants') and product.variants|length > 0 %}
|
| 957 |
<p style="font-size: 1.4rem; font-weight: bold; color: var(--primary-color); margin: 15px 0;">
|
| 958 |
{{ _['from_price'] }} {{ "%.2f"|format(product.variants|map(attribute='price')|min) }} {{ currency_code }}
|
|
@@ -967,6 +987,16 @@ PRODUCT_DETAIL_TEMPLATE = '''
|
|
| 967 |
{% endfor %}
|
| 968 |
</ul>
|
| 969 |
{% endif %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 970 |
<p style="margin-top: 20px;"><strong>{{ _['description'] }}:</strong><br> {{ product.get('description', _['no_description'])|replace('\\n', '<br>')|safe }}</p>
|
| 971 |
</div>
|
| 972 |
<div style="padding: 20px 10px 10px; text-align: center;">
|
|
@@ -983,20 +1013,20 @@ ORDER_TEMPLATE = '''
|
|
| 983 |
<head>
|
| 984 |
<meta charset="UTF-8">
|
| 985 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 986 |
-
<title>{{ _['order_page_title'] }}{{ order.id }} -
|
| 987 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 988 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 989 |
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@400;600;700&family=Georgia&display=swap" rel="stylesheet">
|
| 990 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
| 991 |
<style>
|
| 992 |
:root {
|
| 993 |
-
--primary-color: #
|
| 994 |
-
--primary-dark: #
|
| 995 |
-
--surface-color: #
|
| 996 |
-
--background-color: #
|
| 997 |
--text-color: #F5F5F5;
|
| 998 |
-
--text-color-muted: #
|
| 999 |
-
--border-color: #
|
| 1000 |
}
|
| 1001 |
body { font-family: 'Georgia', serif; background: var(--background-color); color: var(--text-color); line-height: 1.6; padding: 15px; }
|
| 1002 |
.container { max-width: 800px; margin: 20px auto; padding: 30px; background: var(--surface-color); border-radius: 16px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.6); border: 1px solid var(--border-color); }
|
|
@@ -1013,7 +1043,7 @@ ORDER_TEMPLATE = '''
|
|
| 1013 |
.order-summary { margin-top: 30px; padding-top: 20px; border-top: 2px solid var(--primary-color); text-align: right; }
|
| 1014 |
.order-summary p { margin-bottom: 10px; font-size: 1.1rem; }
|
| 1015 |
.order-summary strong { font-size: 1.5rem; color: var(--primary-color); }
|
| 1016 |
-
.customer-info { margin-top: 30px; background-color: rgba(
|
| 1017 |
.customer-info p { margin-bottom: 8px; font-size: 1rem; }
|
| 1018 |
.customer-info strong { color: var(--text-color); }
|
| 1019 |
.actions { margin-top: 30px; text-align: center; }
|
|
@@ -1038,6 +1068,9 @@ ORDER_TEMPLATE = '''
|
|
| 1038 |
<div class="item-details">
|
| 1039 |
<strong>{{ item.name }}</strong>
|
| 1040 |
<span>{{ _['cart_item_variant'] }}: {{ item.variantName }}</span>
|
|
|
|
|
|
|
|
|
|
| 1041 |
<span>{{ "%.2f"|format(item.price) }} {{ currency_code }} × {{ item.quantity }}</span>
|
| 1042 |
</div>
|
| 1043 |
<div class="item-total">
|
|
@@ -1067,7 +1100,7 @@ ORDER_TEMPLATE = '''
|
|
| 1067 |
function sendOrderViaWhatsApp() {
|
| 1068 |
const orderId = '{{ order.id }}';
|
| 1069 |
const orderUrl = `{{ request.url }}`;
|
| 1070 |
-
const whatsappNumber = "
|
| 1071 |
|
| 1072 |
let message = `{{ _['whatsapp_confirm_message_1'] }}%0A%0A`;
|
| 1073 |
message += `*{{ _['whatsapp_confirm_message_2'] }}* ${orderId}%0A`;
|
|
@@ -1095,20 +1128,20 @@ ADMIN_TEMPLATE = '''
|
|
| 1095 |
<head>
|
| 1096 |
<meta charset="UTF-8">
|
| 1097 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 1098 |
-
<title>Админ-панель -
|
| 1099 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 1100 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 1101 |
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@600&family=Georgia&display=swap" rel="stylesheet">
|
| 1102 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
| 1103 |
<style>
|
| 1104 |
:root {
|
| 1105 |
-
--primary-color: #
|
| 1106 |
-
--primary-dark: #
|
| 1107 |
-
--surface-color: #
|
| 1108 |
-
--background-color: #
|
| 1109 |
--text-color: #F5F5F5;
|
| 1110 |
-
--text-color-muted: #
|
| 1111 |
-
--border-color: #
|
| 1112 |
--success-bg: #113d21;
|
| 1113 |
--success-text: #6ee791;
|
| 1114 |
--error-bg: #4d0a0a;
|
|
@@ -1127,7 +1160,7 @@ ADMIN_TEMPLATE = '''
|
|
| 1127 |
|
| 1128 |
label { font-weight: 500; margin-top: 12px; display: block; color: var(--text-color); font-size: 0.9rem;}
|
| 1129 |
input[type="text"], input[type="number"], input[type="password"], input[type="tel"], textarea, select { width: 100%; padding: 10px 12px; margin-top: 6px; border: 1px solid var(--border-color); border-radius: 6px; font-size: 0.95rem; box-sizing: border-box; transition: border-color 0.3s ease; background-color: var(--background-color); color: var(--text-color); }
|
| 1130 |
-
input:focus, textarea:focus, select:focus { border-color: var(--primary-color); outline: none; box-shadow: 0 0 0 3px rgba(
|
| 1131 |
textarea { min-height: 90px; resize: vertical; }
|
| 1132 |
input[type="file"] { padding: 8px; background-color: #222; cursor: pointer; border: 1px solid var(--border-color); border-radius: 6px; }
|
| 1133 |
input[type="checkbox"] { transform: scale(1.2); margin-right: 8px; vertical-align: middle; accent-color: var(--primary-color); }
|
|
@@ -1145,11 +1178,11 @@ ADMIN_TEMPLATE = '''
|
|
| 1145 |
.photo-edit-item { position: relative; }
|
| 1146 |
.photo-edit-item input[type="checkbox"] { position: absolute; top: 0px; right: 0px; transform: scale(1.3); accent-color: #ff453a; cursor: pointer; }
|
| 1147 |
|
| 1148 |
-
.
|
| 1149 |
-
.
|
| 1150 |
-
.remove-
|
| 1151 |
-
.add-
|
| 1152 |
-
.add-
|
| 1153 |
|
| 1154 |
.message { padding: 12px 18px; border-radius: 8px; margin-bottom: 20px; font-size: 0.95rem; border: 1px solid;}
|
| 1155 |
.message.success { background-color: var(--success-bg); color: var(--success-text); border-color: var(--success-text);}
|
|
@@ -1177,8 +1210,8 @@ ADMIN_TEMPLATE = '''
|
|
| 1177 |
<div class="container">
|
| 1178 |
<div class="header">
|
| 1179 |
<div class="logo-title-container" style="display: flex; align-items: center; gap: 15px;">
|
| 1180 |
-
<img src="https://huggingface.co/spaces/
|
| 1181 |
-
<h1><i class="fas fa-tools"></i>
|
| 1182 |
</div>
|
| 1183 |
<a href="{{ url_for('catalog', lang_code='ru') }}" class="button"><i class="fas fa-store"></i> Перейти в каталог</a>
|
| 1184 |
</div>
|
|
@@ -1192,6 +1225,17 @@ ADMIN_TEMPLATE = '''
|
|
| 1192 |
{% endif %}
|
| 1193 |
{% endwith %}
|
| 1194 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1195 |
<div class="section">
|
| 1196 |
<h2><i class="fas fa-history"></i> История заказов</h2>
|
| 1197 |
{% if orders %}
|
|
@@ -1231,86 +1275,55 @@ ADMIN_TEMPLATE = '''
|
|
| 1231 |
{% endif %}
|
| 1232 |
</div>
|
| 1233 |
|
| 1234 |
-
|
| 1235 |
-
|
| 1236 |
-
|
| 1237 |
-
|
| 1238 |
-
|
| 1239 |
-
<
|
| 1240 |
-
<
|
| 1241 |
-
<
|
| 1242 |
-
|
| 1243 |
-
|
| 1244 |
-
|
| 1245 |
-
|
| 1246 |
-
|
| 1247 |
-
|
| 1248 |
-
|
| 1249 |
-
|
| 1250 |
-
|
| 1251 |
-
|
| 1252 |
-
<
|
| 1253 |
-
|
| 1254 |
-
<
|
| 1255 |
-
|
| 1256 |
-
|
| 1257 |
-
|
| 1258 |
-
<input type="hidden" name="category_name" value="{{ category }}">
|
| 1259 |
-
<button type="submit" class="delete-button" style="padding: 6px 12px; font-size: 0.8rem; margin: 0;"><i class="fas fa-trash-alt"></i></button>
|
| 1260 |
-
</form>
|
| 1261 |
-
</div>
|
| 1262 |
-
{% endfor %}
|
| 1263 |
-
</div>
|
| 1264 |
-
{% else %}
|
| 1265 |
-
<p>Категорий пока нет.</p>
|
| 1266 |
-
{% endif %}
|
| 1267 |
-
</div>
|
| 1268 |
-
</div>
|
| 1269 |
-
|
| 1270 |
-
<div class="flex-item">
|
| 1271 |
-
<div class="section">
|
| 1272 |
-
<h2><i class="fas fa-copyright"></i> Бренды</h2>
|
| 1273 |
-
<details>
|
| 1274 |
-
<summary><i class="fas fa-plus-circle"></i> Добавить новый бренд</summary>
|
| 1275 |
-
<div style="padding: 15px;">
|
| 1276 |
-
<form method="POST">
|
| 1277 |
-
<input type="hidden" name="action" value="add_brand">
|
| 1278 |
-
<label for="add_brand_name">Название нового бренда:</label>
|
| 1279 |
-
<input type="text" id="add_brand_name" name="brand_name" required>
|
| 1280 |
-
<button type="submit"><i class="fas fa-plus"></i> Добавить</button>
|
| 1281 |
-
</form>
|
| 1282 |
-
</div>
|
| 1283 |
-
</details>
|
| 1284 |
-
<h3>Существующие бренды:</h3>
|
| 1285 |
-
{% if brands %}
|
| 1286 |
-
<div class="item-list">
|
| 1287 |
-
{% for brand in brands %}
|
| 1288 |
-
<div class="item" style="display: flex; justify-content: space-between; align-items: center; padding: 10px;">
|
| 1289 |
-
<span>{{ brand }}</span>
|
| 1290 |
-
<form method="POST" style="margin: 0;" onsubmit="return confirm('Удалить бренд \'{{ brand }}\'?');">
|
| 1291 |
-
<input type="hidden" name="action" value="delete_brand">
|
| 1292 |
-
<input type="hidden" name="brand_name" value="{{ brand }}">
|
| 1293 |
-
<button type="submit" class="delete-button" style="padding: 6px 12px; font-size: 0.8rem; margin: 0;"><i class="fas fa-trash-alt"></i></button>
|
| 1294 |
-
</form>
|
| 1295 |
-
</div>
|
| 1296 |
-
{% endfor %}
|
| 1297 |
-
</div>
|
| 1298 |
-
{% else %}
|
| 1299 |
-
<p>Брендов пока нет.</p>
|
| 1300 |
-
{% endif %}
|
| 1301 |
</div>
|
|
|
|
| 1302 |
</div>
|
|
|
|
|
|
|
|
|
|
| 1303 |
</div>
|
| 1304 |
|
|
|
|
| 1305 |
<div class="section">
|
| 1306 |
<h2><i class="fas fa-box-open"></i> Товары</h2>
|
| 1307 |
<details>
|
| 1308 |
<summary><i class="fas fa-plus-circle"></i> Добавить новый товар</summary>
|
| 1309 |
<div style="padding: 15px;">
|
| 1310 |
-
<form method="POST" enctype="multipart/form-data">
|
| 1311 |
<input type="hidden" name="action" value="add_product">
|
| 1312 |
<label for="add_name">Название товара *:</label>
|
| 1313 |
<input type="text" id="add_name" name="name" required>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1314 |
<label for="add_description">Описание:</label>
|
| 1315 |
<textarea id="add_description" name="description" rows="4"></textarea>
|
| 1316 |
|
|
@@ -1322,26 +1335,29 @@ ADMIN_TEMPLATE = '''
|
|
| 1322 |
{% endfor %}
|
| 1323 |
</select>
|
| 1324 |
|
| 1325 |
-
<label for="add_brand">Бренд:</label>
|
| 1326 |
-
<select id="add_brand" name="brand">
|
| 1327 |
-
<option value="Без бренда">Без бренда</option>
|
| 1328 |
-
{% for brand in brands %}
|
| 1329 |
-
<option value="{{ brand }}">{{ brand }}</option>
|
| 1330 |
-
{% endfor %}
|
| 1331 |
-
</select>
|
| 1332 |
-
|
| 1333 |
<label for="add_photos">Фотографии (до 10 шт.):</label>
|
| 1334 |
<input type="file" id="add_photos" name="photos" accept="image/*" multiple>
|
| 1335 |
|
| 1336 |
<h4>Варианты и цены *:</h4>
|
| 1337 |
<div id="add-variants-container">
|
| 1338 |
-
<div class="
|
| 1339 |
-
<input type="text" name="variant_names" placeholder="Название варианта (напр.
|
| 1340 |
<input type="number" name="variant_prices" step="0.01" min="0" placeholder="Цена" required>
|
| 1341 |
-
<button type="button" class="remove-
|
| 1342 |
</div>
|
| 1343 |
</div>
|
| 1344 |
-
<button type="button" class="button add-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1345 |
|
| 1346 |
<div style="margin-top: 20px;">
|
| 1347 |
<input type="checkbox" id="add_is_top" name="is_top">
|
|
@@ -1373,8 +1389,8 @@ ADMIN_TEMPLATE = '''
|
|
| 1373 |
{{ product['name'] }}
|
| 1374 |
{% if product.get('is_top', False) %}<span class="status-indicator top-product"><i class="fas fa-star fa-xs"></i> Топ</span>{% endif %}
|
| 1375 |
</h3>
|
|
|
|
| 1376 |
<p style="font-size: 0.9rem; color: var(--text-color-muted);"><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
|
| 1377 |
-
<p style="font-size: 0.9rem; color: var(--text-color-muted);"><strong>Бренд:</strong> {{ product.get('brand', 'Без бренда') }}</p>
|
| 1378 |
</div>
|
| 1379 |
</div>
|
| 1380 |
|
|
@@ -1394,6 +1410,13 @@ ADMIN_TEMPLATE = '''
|
|
| 1394 |
<input type="hidden" name="product_id" value="{{ product.id }}">
|
| 1395 |
<label>Название *:</label>
|
| 1396 |
<input type="text" name="name" value="{{ product['name'] }}" required>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1397 |
<label>Описание:</label>
|
| 1398 |
<textarea name="description" rows="4">{{ product.get('description', '') }}</textarea>
|
| 1399 |
|
|
@@ -1404,14 +1427,6 @@ ADMIN_TEMPLATE = '''
|
|
| 1404 |
<option value="{{ category }}" {% if product.get('category') == category %}selected{% endif %}>{{ category }}</option>
|
| 1405 |
{% endfor %}
|
| 1406 |
</select>
|
| 1407 |
-
|
| 1408 |
-
<label>Бренд:</label>
|
| 1409 |
-
<select name="brand">
|
| 1410 |
-
<option value="Без бренда" {% if product.get('brand', 'Без бренда') == 'Без бренда' %}selected{% endif %}>Без бренда</option>
|
| 1411 |
-
{% for brand in brands %}
|
| 1412 |
-
<option value="{{ brand }}" {% if product.get('brand') == brand %}selected{% endif %}>{{ brand }}</option>
|
| 1413 |
-
{% endfor %}
|
| 1414 |
-
</select>
|
| 1415 |
|
| 1416 |
<label>Текущие фотографии (отметьте для удаления):</label>
|
| 1417 |
<div class="photo-preview-edit">
|
|
@@ -1433,14 +1448,33 @@ ADMIN_TEMPLATE = '''
|
|
| 1433 |
<h4>Варианты и цены *:</h4>
|
| 1434 |
<div id="edit-variants-container-{{ product.id }}">
|
| 1435 |
{% for variant in product.get('variants', []) %}
|
| 1436 |
-
<div class="
|
| 1437 |
<input type="text" name="variant_names" value="{{ variant.name }}" required>
|
| 1438 |
<input type="number" name="variant_prices" step="0.01" min="0" value="{{ variant.price }}" required>
|
| 1439 |
-
<button type="button" class="remove-
|
| 1440 |
</div>
|
| 1441 |
{% endfor %}
|
| 1442 |
</div>
|
| 1443 |
-
<button type="button" class="button add-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1444 |
|
| 1445 |
<div style="margin-top: 20px;">
|
| 1446 |
<input type="checkbox" id="edit_is_top_{{ product.id }}" name="is_top" {% if product.get('is_top', False) %}checked{% endif %}>
|
|
@@ -1474,23 +1508,43 @@ ADMIN_TEMPLATE = '''
|
|
| 1474 |
const form = document.getElementById(formId);
|
| 1475 |
form.style.display = form.style.display === 'none' ? 'block' : 'none';
|
| 1476 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1477 |
|
| 1478 |
function addVariantInput(containerId) {
|
| 1479 |
const container = document.getElementById(containerId);
|
| 1480 |
const newInputGroup = document.createElement('div');
|
| 1481 |
-
newInputGroup.className = '
|
| 1482 |
newInputGroup.innerHTML = `
|
| 1483 |
<input type="text" name="variant_names" placeholder="Название варианта" required>
|
| 1484 |
<input type="number" name="variant_prices" step="0.01" min="0" placeholder="Цена" required>
|
| 1485 |
-
<button type="button" class="remove-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1486 |
`;
|
| 1487 |
container.appendChild(newInputGroup);
|
| 1488 |
}
|
| 1489 |
|
| 1490 |
-
function
|
| 1491 |
-
const group = button.closest('.
|
| 1492 |
const container = group.parentNode;
|
| 1493 |
-
if (container.children.length > 1) {
|
| 1494 |
group.remove();
|
| 1495 |
} else {
|
| 1496 |
alert("Должен быть хотя бы один вариант.");
|
|
@@ -1520,7 +1574,7 @@ def catalog(lang_code):
|
|
| 1520 |
data = load_data()
|
| 1521 |
all_products = data.get('products', [])
|
| 1522 |
categories = sorted(data.get('categories', []))
|
| 1523 |
-
|
| 1524 |
|
| 1525 |
products_in_stock = [p for p in all_products if p.get('variants')]
|
| 1526 |
|
|
@@ -1533,12 +1587,12 @@ def catalog(lang_code):
|
|
| 1533 |
CATALOG_TEMPLATE,
|
| 1534 |
products=products_sorted,
|
| 1535 |
categories=categories,
|
| 1536 |
-
brands=brands,
|
| 1537 |
repo_id=REPO_ID,
|
| 1538 |
store_addresses=STORE_ADDRESSES,
|
| 1539 |
currency_code=CURRENCY_CODE,
|
| 1540 |
lang_code=g.lang_code,
|
| 1541 |
-
_=g.translations
|
|
|
|
| 1542 |
)
|
| 1543 |
|
| 1544 |
@app.route('/<lang_code>/product/<int:index>')
|
|
@@ -1589,7 +1643,7 @@ def create_order():
|
|
| 1589 |
|
| 1590 |
try:
|
| 1591 |
price = float(item['price'])
|
| 1592 |
-
quantity =
|
| 1593 |
if price < 0 or quantity <= 0: raise ValueError("Invalid price/qty")
|
| 1594 |
|
| 1595 |
total_price += price * quantity
|
|
@@ -1601,6 +1655,7 @@ def create_order():
|
|
| 1601 |
"price": price,
|
| 1602 |
"quantity": quantity,
|
| 1603 |
"variantName": item.get('variantName', 'N/A'),
|
|
|
|
| 1604 |
"photo": photo_name,
|
| 1605 |
"photo_url": photo_url
|
| 1606 |
})
|
|
@@ -1631,13 +1686,16 @@ def create_order():
|
|
| 1631 |
def view_order(lang_code, order_id):
|
| 1632 |
data = load_data()
|
| 1633 |
order = data.get('orders', {}).get(order_id)
|
|
|
|
|
|
|
| 1634 |
status_map = STATUS_MAPS.get(lang_code, STATUS_MAPS['ru'])
|
| 1635 |
-
return render_template_string(ORDER_TEMPLATE, order=order, status_map=status_map, currency_code=CURRENCY_CODE, lang_code=g.lang_code, _=g.translations)
|
| 1636 |
|
| 1637 |
@app.route('/admin', methods=['GET', 'POST'])
|
| 1638 |
def admin():
|
| 1639 |
data = load_data()
|
| 1640 |
if 'orders' not in data: data['orders'] = {}
|
|
|
|
| 1641 |
|
| 1642 |
needs_save = False
|
| 1643 |
for p in data.get('products', []):
|
|
@@ -1650,7 +1708,13 @@ def admin():
|
|
| 1650 |
if request.method == 'POST':
|
| 1651 |
action = request.form.get('action')
|
| 1652 |
try:
|
| 1653 |
-
if action == '
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1654 |
order_id = request.form.get('order_id')
|
| 1655 |
new_status = request.form.get('new_status')
|
| 1656 |
if order_id in data['orders'] and new_status in STATUS_MAPS['ru']:
|
|
@@ -1681,31 +1745,12 @@ def admin():
|
|
| 1681 |
else:
|
| 1682 |
flash("Категория не найдена.", 'error')
|
| 1683 |
|
| 1684 |
-
elif action == 'add_brand':
|
| 1685 |
-
brand_name = request.form.get('brand_name', '').strip()
|
| 1686 |
-
if brand_name and brand_name not in data['brands']:
|
| 1687 |
-
data['brands'].append(brand_name)
|
| 1688 |
-
save_data(data)
|
| 1689 |
-
flash(f"Бренд '{brand_name}' добавлен.", 'success')
|
| 1690 |
-
else:
|
| 1691 |
-
flash("Ошибка: Бренд пуст или уже существует.", 'error')
|
| 1692 |
-
|
| 1693 |
-
elif action == 'delete_brand':
|
| 1694 |
-
brand_to_delete = request.form.get('brand_name')
|
| 1695 |
-
if brand_to_delete in data['brands']:
|
| 1696 |
-
data['brands'].remove(brand_to_delete)
|
| 1697 |
-
for product in data['products']:
|
| 1698 |
-
if product.get('brand') == brand_to_delete:
|
| 1699 |
-
product['brand'] = 'Без бренда'
|
| 1700 |
-
save_data(data)
|
| 1701 |
-
flash(f"Бренд '{brand_to_delete}' удален.", 'success')
|
| 1702 |
-
else:
|
| 1703 |
-
flash("Бренд не найден.", 'error')
|
| 1704 |
-
|
| 1705 |
elif action == 'add_product' or action == 'edit_product':
|
| 1706 |
name = request.form.get('name', '').strip()
|
| 1707 |
-
|
| 1708 |
-
|
|
|
|
|
|
|
| 1709 |
return redirect(url_for('admin'))
|
| 1710 |
|
| 1711 |
variant_names = [v.strip() for v in request.form.getlist('variant_names') if v.strip()]
|
|
@@ -1725,13 +1770,22 @@ def admin():
|
|
| 1725 |
flash("Неверный формат цены в вариантах.", 'error')
|
| 1726 |
return redirect(url_for('admin'))
|
| 1727 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1728 |
product_data = {
|
| 1729 |
'name': name,
|
|
|
|
| 1730 |
'description': request.form.get('description', '').strip(),
|
| 1731 |
'category': request.form.get('category'),
|
| 1732 |
-
'brand': request.form.get('brand'),
|
| 1733 |
'is_top': 'is_top' in request.form,
|
| 1734 |
-
'variants': variants
|
|
|
|
| 1735 |
}
|
| 1736 |
|
| 1737 |
newly_uploaded_photos = []
|
|
@@ -1829,8 +1883,8 @@ def admin():
|
|
| 1829 |
ADMIN_TEMPLATE,
|
| 1830 |
products=sorted(current_data.get('products', []), key=lambda p: p.get('name', '').lower()),
|
| 1831 |
categories=sorted(current_data.get('categories', [])),
|
| 1832 |
-
brands=sorted(current_data.get('brands', [])),
|
| 1833 |
orders=list(current_data.get('orders', {}).values()),
|
|
|
|
| 1834 |
status_map_ru=STATUS_MAPS['ru'],
|
| 1835 |
repo_id=REPO_ID,
|
| 1836 |
store_addresses=STORE_ADDRESSES,
|
|
|
|
| 28 |
|
| 29 |
STORE_ADDRESSES = [
|
| 30 |
"Город Алматы , Алатау 1 , блок 4 , бутик 112",
|
|
|
|
| 31 |
]
|
| 32 |
|
| 33 |
|
|
|
|
| 54 |
|
| 55 |
translations = {
|
| 56 |
'ru': {
|
| 57 |
+
'page_title': "Esmira - Каталог тканей и одежды",
|
| 58 |
+
'header_title': "Esmira",
|
| 59 |
'our_addresses': "Наши адреса в г. Алматы",
|
| 60 |
'search_placeholder': "Поиск по названию или описанию...",
|
| 61 |
'all_filter': "Все",
|
|
|
|
| 67 |
'product_load_error': "Не удалось загрузить информацию о товаре.",
|
| 68 |
'specify_details': "Укажите детали",
|
| 69 |
'variant_label': "Вариант:",
|
| 70 |
+
'size_label': "Размер:",
|
| 71 |
+
'quantity_label': "Количество (в метрах для тканей):",
|
| 72 |
'confirm_add_to_cart': "Добавить в корзину",
|
| 73 |
'your_cart': "Ваша корзина",
|
| 74 |
'cart_is_empty': "Ваша корзина пуста.",
|
|
|
|
| 76 |
'clear_cart_button': "Очистить",
|
| 77 |
'formulate_order_button': "Оформить заказ",
|
| 78 |
'cart_item_variant': "Вариант",
|
| 79 |
+
'cart_item_size': "Размер",
|
| 80 |
'remove_item_title': "Удалить товар",
|
| 81 |
'clear_cart_confirm': "Вы уверены, что хотите очистить корзину?",
|
| 82 |
'cart_is_empty_alert': "Корзина пуста!",
|
|
|
|
| 84 |
'add_to_cart_notification': "добавлен в корзину!",
|
| 85 |
'no_products_found': "По вашему запросу товары не найдены.",
|
| 86 |
'category': "Категория",
|
| 87 |
+
'product_type': "Тип товара",
|
| 88 |
+
'fabrics': "Ткани",
|
| 89 |
+
'clothing': "Одежда",
|
| 90 |
'no_category': "Без категории",
|
|
|
|
| 91 |
'available_variants': "Доступные варианты:",
|
| 92 |
+
'available_sizes': "Доступные размеры:",
|
| 93 |
'description': "Описание:",
|
| 94 |
'no_description': "Описание отсутствует.",
|
| 95 |
'order_page_title': "Заказ №",
|
|
|
|
| 103 |
'return_to_catalog': "Вернуться в каталог",
|
| 104 |
'order_not_found_error': "Ошибка",
|
| 105 |
'order_not_found_message': "Заказ с таким ID не найден.",
|
| 106 |
+
'whatsapp_confirm_message_1': "Здравствуйте! Хочу подтвердить свой заказ в Esmira:",
|
| 107 |
'whatsapp_confirm_message_2': "Номер заказа:",
|
| 108 |
'whatsapp_confirm_message_3': "Ссылка на заказ:",
|
| 109 |
'whatsapp_confirm_message_4': "Пожалуйста, свяжитесь со мной для уточнения деталей.",
|
| 110 |
},
|
| 111 |
'kz': {
|
| 112 |
+
'page_title': "Esmira - Маталар мен киімдер каталогы",
|
| 113 |
+
'header_title': "Esmira",
|
| 114 |
'our_addresses': "Алматы қаласындағы мекенжайларымыз",
|
| 115 |
'search_placeholder': "Аты немесе сипаттамасы бойынша іздеу...",
|
| 116 |
'all_filter': "Барлығы",
|
|
|
|
| 122 |
'product_load_error': "Тауар туралы ақпаратты жүктеу мүмкін болмады.",
|
| 123 |
'specify_details': "Мәліметтерді көрсетіңіз",
|
| 124 |
'variant_label': "Нұсқа:",
|
| 125 |
+
'size_label': "Өлшем:",
|
| 126 |
+
'quantity_label': "Саны (маталар үшін метрмен):",
|
| 127 |
'confirm_add_to_cart': "Себетке қосу",
|
| 128 |
'your_cart': "Сіздің себетіңіз",
|
| 129 |
'cart_is_empty': "Сіздің себетіңіз бос.",
|
|
|
|
| 131 |
'clear_cart_button': "Тазарту",
|
| 132 |
'formulate_order_button': "Тапсырыс беру",
|
| 133 |
'cart_item_variant': "Нұсқа",
|
| 134 |
+
'cart_item_size': "Өлшем",
|
| 135 |
'remove_item_title': "Тауарды жою",
|
| 136 |
'clear_cart_confirm': "Себетті тазалағыңыз келетініне сенімдісіз бе?",
|
| 137 |
'cart_is_empty_alert': "Себет бос!",
|
|
|
|
| 139 |
'add_to_cart_notification': "себетке қосылды!",
|
| 140 |
'no_products_found': "Сіздің сұранысыңыз бойынша тауарлар табылмады.",
|
| 141 |
'category': "Санат",
|
| 142 |
+
'product_type': "Тауар түрі",
|
| 143 |
+
'fabrics': "Маталар",
|
| 144 |
+
'clothing': "Киім",
|
| 145 |
'no_category': "Санатсыз",
|
|
|
|
| 146 |
'available_variants': "Қолжетімді нұсқалар:",
|
| 147 |
+
'available_sizes': "Қолжетімді өлшемдер:",
|
| 148 |
'description': "Сипаттама:",
|
| 149 |
'no_description': "Сипаттама жоқ.",
|
| 150 |
'order_page_title': "Тапсырыс №",
|
|
|
|
| 158 |
'return_to_catalog': "Каталогқа оралу",
|
| 159 |
'order_not_found_error': "Қате",
|
| 160 |
'order_not_found_message': "Мұндай ID-мен тапсырыс табылмады.",
|
| 161 |
+
'whatsapp_confirm_message_1': "Сәлеметсіз бе! Esmira-дағы тапсырысымды растағым келеді:",
|
| 162 |
'whatsapp_confirm_message_2': "Тапсырыс нөмірі:",
|
| 163 |
'whatsapp_confirm_message_3': "Тапсырысқа сілтеме:",
|
| 164 |
'whatsapp_confirm_message_4': "Мәліметтерді нақтылау үшін менімен хабарласуыңызды сұраймын.",
|
|
|
|
| 208 |
try:
|
| 209 |
if file_name == DATA_FILE:
|
| 210 |
with open(file_name, 'w', encoding='utf-8') as f:
|
| 211 |
+
json.dump({'products': [], 'categories': [], 'orders': {}, 'settings': {}}, f)
|
| 212 |
logging.info(f"Created empty local file {file_name} because it was not found on HF.")
|
| 213 |
except Exception as create_e:
|
| 214 |
logging.error(f"Failed to create empty local file {file_name}: {create_e}")
|
|
|
|
| 272 |
|
| 273 |
|
| 274 |
def load_data():
|
| 275 |
+
default_data = {'products': [], 'categories': [], 'orders': {}, 'settings': {'whatsapp_number': '77073363943'}}
|
| 276 |
try:
|
| 277 |
with open(DATA_FILE, 'r', encoding='utf-8') as file:
|
| 278 |
data = json.load(file)
|
|
|
|
| 282 |
raise FileNotFoundError
|
| 283 |
if 'products' not in data: data['products'] = []
|
| 284 |
if 'categories' not in data: data['categories'] = []
|
|
|
|
| 285 |
if 'orders' not in data: data['orders'] = {}
|
| 286 |
+
if 'settings' not in data: data['settings'] = {'whatsapp_number': '77073363943'}
|
| 287 |
return data
|
| 288 |
except FileNotFoundError:
|
| 289 |
logging.warning(f"Local file {DATA_FILE} not found. Attempting download from HF.")
|
|
|
|
| 300 |
return default_data
|
| 301 |
if 'products' not in data: data['products'] = []
|
| 302 |
if 'categories' not in data: data['categories'] = []
|
|
|
|
| 303 |
if 'orders' not in data: data['orders'] = {}
|
| 304 |
+
if 'settings' not in data: data['settings'] = {'whatsapp_number': '77073363943'}
|
| 305 |
return data
|
| 306 |
except FileNotFoundError:
|
| 307 |
logging.error(f"File {DATA_FILE} still not found even after download reported success. Using default.")
|
|
|
|
| 330 |
return
|
| 331 |
if 'products' not in data: data['products'] = []
|
| 332 |
if 'categories' not in data: data['categories'] = []
|
|
|
|
| 333 |
if 'orders' not in data: data['orders'] = {}
|
| 334 |
+
if 'settings' not in data: data['settings'] = {}
|
| 335 |
|
| 336 |
with open(DATA_FILE, 'w', encoding='utf-8') as file:
|
| 337 |
json.dump(data, file, ensure_ascii=False, indent=4)
|
|
|
|
| 356 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.css">
|
| 357 |
<style>
|
| 358 |
:root {
|
| 359 |
+
--primary-color: #B22222;
|
| 360 |
+
--primary-dark: #8B0000;
|
| 361 |
+
--surface-color: #2b2b2b;
|
| 362 |
+
--background-color: #1c1c1c;
|
| 363 |
--text-color: #F5F5F5;
|
| 364 |
+
--text-color-muted: #a0a0a0;
|
| 365 |
+
--border-color: #444;
|
| 366 |
--success-color: #28a745;
|
| 367 |
}
|
| 368 |
* { margin: 0; padding: 0; box-sizing: border-box; }
|
| 369 |
+
body {
|
| 370 |
+
font-family: 'Georgia', serif;
|
| 371 |
+
background: var(--background-color);
|
| 372 |
+
color: var(--text-color);
|
| 373 |
+
line-height: 1.6;
|
| 374 |
+
transition: background-color 0.3s;
|
| 375 |
}
|
| 376 |
.container { max-width: 100%; margin: 0 auto; padding: 0; }
|
| 377 |
.content-area { padding: 20px; }
|
| 378 |
|
| 379 |
+
.header { display: flex; justify-content: space-between; align-items: center; padding: 15px 20px; background: rgba(28, 28, 28, 0.8); backdrop-filter: blur(10px); border-bottom: 1px solid var(--border-color); position: sticky; top: 0; z-index: 1000; }
|
| 380 |
.logo-title-container { display: flex; align-items: center; gap: 15px; }
|
| 381 |
+
.logo-title-container img { height: 45px; width: 45px; border-radius: 50%; object-fit: cover; }
|
| 382 |
.header h1 { font-family: 'Cormorant Garamond', serif; font-size: 1.8rem; font-weight: 700; color: var(--text-color); }
|
| 383 |
|
| 384 |
.lang-switcher { display: flex; gap: 5px; background-color: var(--surface-color); padding: 5px; border-radius: 50px; }
|
|
|
|
| 391 |
|
| 392 |
.search-container { padding: 0 20px 20px; }
|
| 393 |
#search-input { width: 100%; padding: 12px 20px; font-size: 1rem; border: 1px solid var(--border-color); border-radius: 50px; outline: none; transition: all 0.3s; background-color: var(--surface-color); color: var(--text-color); }
|
| 394 |
+
#search-input:focus { border-color: var(--primary-color); box-shadow: 0 0 0 4px rgba(178, 34, 34, 0.2); }
|
| 395 |
|
| 396 |
.filters-wrapper { margin: 0 20px 20px; display: flex; flex-direction: column; gap: 15px; }
|
| 397 |
.filters-container { display: flex; overflow-x: auto; gap: 10px; padding-bottom: 10px; scrollbar-width: none; -ms-overflow-style: none; }
|
| 398 |
.filters-container::-webkit-scrollbar { display: none; }
|
| 399 |
.filter-label { font-size: 0.9rem; color: var(--text-color-muted); margin-left: 5px; }
|
| 400 |
+
.filter-btn { padding: 8px 18px; border: 1px solid var(--border-color); border-radius: 50px; background-color: transparent; cursor: pointer; transition: all 0.3s ease; font-size: 0.9rem; font-weight: 400; color: var(--text-color-muted); white-space: nowrap; }
|
| 401 |
+
.filter-btn.active, .filter-btn:hover { background-color: var(--primary-color); color: #fff; border-color: var(--primary-color); font-weight: 500; box-shadow: 0 2px 8px rgba(178, 34, 34, 0.3); transform: translateY(-2px); }
|
| 402 |
|
| 403 |
.products-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 15px; padding: 0 20px 120px; }
|
| 404 |
@media (min-width: 600px) { .products-grid { grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 20px; } }
|
| 405 |
|
| 406 |
.product { background: var(--surface-color); border-radius: 16px; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4); transition: all 0.3s ease; overflow: hidden; display: flex; flex-direction: column; height: 100%; position: relative; border: 1px solid var(--border-color); }
|
| 407 |
+
.product:hover { transform: translateY(-5px); box-shadow: 0 8px 25px rgba(178, 34, 34, 0.15); border-color: var(--primary-color); }
|
| 408 |
|
| 409 |
.product-image { width: 100%; aspect-ratio: 1 / 1; background-color: #000; display: flex; justify-content: center; align-items: center; padding: 10px; }
|
| 410 |
.product-image img { max-width: 100%; max-height: 100%; object-fit: contain; transition: transform 0.3s ease; }
|
|
|
|
| 417 |
.product-actions { padding: 0 15px 15px; }
|
| 418 |
|
| 419 |
.product-button { display: inline-flex; align-items: center; justify-content: center; width: 100%; padding: 10px; border: none; border-radius: 50px; background-color: var(--primary-color); color: #fff; font-size: 0.9rem; font-weight: 500; cursor: pointer; transition: all 0.3s ease; text-decoration: none; text-transform: uppercase; letter-spacing: 0.5px; }
|
| 420 |
+
.product-button:hover { background-color: var(--primary-dark); box-shadow: 0 4px 10px rgba(178, 34, 34, 0.4); }
|
| 421 |
.product-button i { margin-right: 8px; }
|
| 422 |
|
| 423 |
+
.fab { position: fixed; background-color: var(--primary-color); color: #fff; border: none; border-radius: 50%; width: 55px; height: 55px; font-size: 1.5rem; cursor: pointer; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 15px rgba(178, 34, 34, 0.4); z-index: 1000; transition: transform 0.2s ease; }
|
| 424 |
.fab:hover { transform: scale(1.1); }
|
| 425 |
#cart-button { bottom: 20px; right: 20px; display: none; }
|
| 426 |
#cart-button span { position: absolute; top: -2px; right: -2px; background-color: #dc3545; color: white; border-radius: 50%; padding: 3px 7px; font-size: 0.75rem; font-weight: bold; min-width: 22px; text-align: center; }
|
|
|
|
| 443 |
.cart-item-remove { background:none; border:none; color:#FF453A; cursor:pointer; font-size: 1.5rem; line-height: 1; transition: color 0.2s; }
|
| 444 |
.cart-item-remove:hover { color: #ff0000; }
|
| 445 |
|
| 446 |
+
.quantity-input, .variant-select, .size-select { width: 100%; padding: 12px; border: 1px solid var(--border-color); border-radius: 8px; font-size: 1rem; margin-top: 8px; margin-bottom: 20px; box-sizing: border-box; background-color: var(--background-color); color: var(--text-color); }
|
| 447 |
|
| 448 |
.cart-summary { margin-top: 25px; text-align: right; border-top: 1px solid var(--border-color); padding-top: 20px; }
|
| 449 |
.cart-summary strong { font-size: 1.4rem; color: var(--primary-color); }
|
|
|
|
| 455 |
.notification { position: fixed; bottom: 80px; left: 50%; transform: translateX(-50%); background-color: var(--primary-color); color: #fff; padding: 12px 25px; border-radius: 50px; box-shadow: 0 4px 15px rgba(0,0,0,0.5); z-index: 1002; opacity: 0; transition: all 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94); font-size: 0.95rem; font-weight: 500; }
|
| 456 |
.notification.show { opacity: 1; bottom: 90px; }
|
| 457 |
.no-results-message { grid-column: 1 / -1; text-align: center; padding: 50px; font-size: 1.2rem; color: var(--text-color-muted); }
|
| 458 |
+
.top-product-indicator { position: absolute; top: 10px; right: 10px; background: linear-gradient(135deg, #B22222, #8B0000); color: #fff; padding: 3px 8px; font-size: 0.7rem; border-radius: 50px; font-weight: bold; z-index: 10; display: flex; align-items: center; gap: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.5); }
|
| 459 |
|
| 460 |
#whatsapp-fab { bottom: 20px; left: 20px; background-color: #25D366; color: white; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 461 |
</style>
|
| 462 |
</head>
|
| 463 |
<body>
|
| 464 |
<div class="container">
|
| 465 |
<header class="header">
|
| 466 |
<div class="logo-title-container">
|
| 467 |
+
<img src="https://huggingface.co/spaces/esmira-tkani/admin/resolve/main/Screenshot_20251225-134859.png" alt="Esmira Logo">
|
| 468 |
<h1>{{ _['header_title'] }}</h1>
|
| 469 |
</div>
|
| 470 |
<div class="lang-switcher">
|
|
|
|
| 487 |
|
| 488 |
<div class="filters-wrapper">
|
| 489 |
<div>
|
| 490 |
+
<span class="filter-label">{{ _['product_type'] }}:</span>
|
| 491 |
<div class="filters-container">
|
| 492 |
+
<button class="filter-btn type-filter active" data-type="all">{{ _['all_filter'] }}</button>
|
| 493 |
+
<button class="filter-btn type-filter" data-type="Ткани">{{ _['fabrics'] }}</button>
|
| 494 |
+
<button class="filter-btn type-filter" data-type="Одежда">{{ _['clothing'] }}</button>
|
|
|
|
| 495 |
</div>
|
| 496 |
</div>
|
|
|
|
| 497 |
<div>
|
| 498 |
+
<span class="filter-label">{{ _['category'] }}:</span>
|
| 499 |
<div class="filters-container">
|
| 500 |
+
<button class="filter-btn category-filter active" data-category="all">{{ _['all_filter'] }}</button>
|
| 501 |
+
{% for category in categories %}
|
| 502 |
+
<button class="filter-btn category-filter" data-category="{{ category }}">{{ category }}</button>
|
| 503 |
{% endfor %}
|
| 504 |
</div>
|
| 505 |
</div>
|
|
|
|
| 506 |
</div>
|
| 507 |
|
| 508 |
|
|
|
|
| 512 |
data-name="{{ product['name']|lower }}"
|
| 513 |
data-description="{{ product.get('description', '')|lower }}"
|
| 514 |
data-category="{{ product.get('category', _['no_category']) }}"
|
| 515 |
+
data-type="{{ product.get('type') }}">
|
| 516 |
{% if product.get('is_top', False) %}
|
| 517 |
<span class="top-product-indicator"><i class="fas fa-star fa-xs"></i> {{ _['top_product'] }}</span>
|
| 518 |
{% endif %}
|
|
|
|
| 559 |
<div class="modal-content">
|
| 560 |
<span class="close" onclick="closeModal('quantityModal')" aria-label="Закрыть">×</span>
|
| 561 |
<h2>{{ _['specify_details'] }}</h2>
|
| 562 |
+
|
| 563 |
<label for="variantSelect">{{ _['variant_label'] }}</label>
|
| 564 |
<select id="variantSelect" class="variant-select"></select>
|
| 565 |
|
| 566 |
+
<div id="size-selection-area" style="display: none;">
|
| 567 |
+
<label for="sizeSelect">{{ _['size_label'] }}</label>
|
| 568 |
+
<select id="sizeSelect" class="size-select"></select>
|
| 569 |
+
</div>
|
| 570 |
+
|
| 571 |
<label for="quantityInput">{{ _['quantity_label'] }}</label>
|
| 572 |
+
<input type="number" id="quantityInput" class="quantity-input" min="0.1" step="0.1" value="1">
|
| 573 |
|
| 574 |
<button class="product-button" onclick="confirmAddToCart()"><i class="fas fa-check"></i> {{ _['confirm_add_to_cart'] }}</button>
|
| 575 |
</div>
|
|
|
|
| 599 |
<span id="cart-count">0</span>
|
| 600 |
</button>
|
| 601 |
|
| 602 |
+
<a id="whatsapp-fab" class="fab" href="https://api.whatsapp.com/send?phone={{ settings.whatsapp_number }}" target="_blank" rel="noopener noreferrer"><i class="fab fa-whatsapp"></i></a>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 603 |
|
| 604 |
<div id="notification-placeholder"></div>
|
| 605 |
|
|
|
|
| 609 |
const repoId = '{{ repo_id }}';
|
| 610 |
const currencyCode = '{{ currency_code }}';
|
| 611 |
let selectedProductIndex = null;
|
| 612 |
+
let cart = JSON.parse(localStorage.getItem('esmiraCart') || '[]');
|
| 613 |
const langCode = '{{ lang_code }}';
|
| 614 |
const translations = {{ _|tojson }};
|
| 615 |
|
|
|
|
| 626 |
document.body.style.overflow = 'auto';
|
| 627 |
}
|
| 628 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 629 |
|
| 630 |
function loadProductDetails(index) {
|
| 631 |
const modalContent = document.getElementById('modalContent');
|
|
|
|
| 652 |
});
|
| 653 |
}
|
| 654 |
|
| 655 |
+
function setupOptionSelect(selectId, options, textFormatter) {
|
| 656 |
+
const select = document.getElementById(selectId);
|
| 657 |
select.innerHTML = '';
|
| 658 |
+
if (options && options.length > 0) {
|
| 659 |
+
options.forEach((option, index) => {
|
| 660 |
+
const opt = document.createElement('option');
|
| 661 |
+
opt.value = index;
|
| 662 |
+
opt.text = textFormatter(option);
|
| 663 |
+
select.appendChild(opt);
|
| 664 |
});
|
| 665 |
+
select.parentElement.style.display = 'block';
|
| 666 |
} else {
|
| 667 |
+
select.parentElement.style.display = 'none';
|
| 668 |
}
|
| 669 |
}
|
| 670 |
|
|
|
|
| 673 |
const product = products[index];
|
| 674 |
if (!product) return;
|
| 675 |
|
| 676 |
+
setupOptionSelect('variantSelect', product.variants, v => `${v.name} - ${v.price.toFixed(2)} ${currencyCode}`);
|
| 677 |
+
|
| 678 |
+
const sizeArea = document.getElementById('size-selection-area');
|
| 679 |
+
const quantityInput = document.getElementById('quantityInput');
|
| 680 |
+
|
| 681 |
+
if (product.type === 'Одежда') {
|
| 682 |
+
setupOptionSelect('sizeSelect', product.sizes, s => s.name);
|
| 683 |
+
sizeArea.style.display = 'block';
|
| 684 |
+
quantityInput.step = "1";
|
| 685 |
+
quantityInput.min = "1";
|
| 686 |
+
quantityInput.value = 1;
|
| 687 |
+
} else {
|
| 688 |
+
sizeArea.style.display = 'none';
|
| 689 |
+
quantityInput.step = "0.1";
|
| 690 |
+
quantityInput.min = "0.1";
|
| 691 |
+
quantityInput.value = 1;
|
| 692 |
+
}
|
| 693 |
+
|
| 694 |
document.getElementById('quantityModal').style.display = 'block';
|
| 695 |
document.body.style.overflow = 'hidden';
|
| 696 |
}
|
|
|
|
| 698 |
function confirmAddToCart() {
|
| 699 |
if (selectedProductIndex === null) return;
|
| 700 |
|
| 701 |
+
const quantityInput = document.getElementById('quantityInput');
|
|
|
|
| 702 |
const product = products[selectedProductIndex];
|
| 703 |
+
|
| 704 |
+
const quantity = product.type === 'Ткани' ? parseFloat(quantityInput.value) : parseInt(quantityInput.value);
|
| 705 |
+
|
| 706 |
+
if (isNaN(quantity) || quantity <= 0) {
|
| 707 |
+
alert("Пожалуйста, укажите корректное количество.");
|
| 708 |
return;
|
| 709 |
}
|
| 710 |
|
| 711 |
+
const variantIndex = document.getElementById('variantSelect').value;
|
| 712 |
const selectedVariant = product.variants[variantIndex];
|
| 713 |
+
|
| 714 |
+
let selectedSize = null;
|
| 715 |
+
let sizeName = null;
|
| 716 |
+
if (product.type === 'Одежда') {
|
| 717 |
+
const sizeIndex = document.getElementById('sizeSelect').value;
|
| 718 |
+
if(product.sizes && product.sizes.length > 0) {
|
| 719 |
+
selectedSize = product.sizes[sizeIndex];
|
| 720 |
+
sizeName = selectedSize.name;
|
| 721 |
+
} else {
|
| 722 |
+
alert("Ошибка: Размеры для этого товара не найдены.");
|
| 723 |
+
return;
|
| 724 |
+
}
|
| 725 |
+
}
|
| 726 |
|
| 727 |
+
if (!product || !selectedVariant) {
|
| 728 |
+
alert("Ошибка: Варианты для этого товара не найдены.");
|
| 729 |
return;
|
| 730 |
}
|
| 731 |
|
| 732 |
+
const cartItemId = `${product.id}-${selectedVariant.name}${sizeName ? '-' + sizeName : ''}`;
|
| 733 |
const existingItem = cart.find(item => item.id === cartItemId);
|
| 734 |
|
| 735 |
if (existingItem) {
|
|
|
|
| 741 |
name: product.name,
|
| 742 |
price: selectedVariant.price,
|
| 743 |
variantName: selectedVariant.name,
|
| 744 |
+
sizeName: sizeName,
|
| 745 |
photo: product.photos && product.photos.length > 0 ? product.photos[0] : null,
|
| 746 |
quantity: quantity
|
| 747 |
});
|
| 748 |
}
|
| 749 |
|
| 750 |
+
localStorage.setItem('esmiraCart', JSON.stringify(cart));
|
| 751 |
closeModal('quantityModal');
|
| 752 |
updateCartButton();
|
| 753 |
+
showNotification(`${product.name} ${translations['add_to_cart_notification']}`);
|
| 754 |
}
|
| 755 |
|
| 756 |
function updateCartButton() {
|
| 757 |
const cartCountElement = document.getElementById('cart-count');
|
| 758 |
const cartButton = document.getElementById('cart-button');
|
| 759 |
+
const totalItems = cart.length;
|
| 760 |
|
| 761 |
if (totalItems > 0) {
|
| 762 |
cartCountElement.textContent = totalItems;
|
|
|
|
| 780 |
total += itemTotal;
|
| 781 |
const photoUrl = item.photo ? `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${item.photo}` : 'https://via.placeholder.com/65x65.png?text=N/A';
|
| 782 |
|
| 783 |
+
let details = `<p class="cart-item-price">${translations['cart_item_variant']}: ${item.variantName}</p>`;
|
| 784 |
+
if (item.sizeName) {
|
| 785 |
+
details += `<p class="cart-item-price">${translations['cart_item_size']}: ${item.sizeName}</p>`;
|
| 786 |
+
}
|
| 787 |
+
details += `<p class="cart-item-price">${item.price.toFixed(2)} ${currencyCode} × ${item.quantity}</p>`;
|
| 788 |
+
|
| 789 |
return `
|
| 790 |
<div class="cart-item">
|
| 791 |
<img src="${photoUrl}" alt="${item.name}">
|
| 792 |
<div class="cart-item-details">
|
| 793 |
<strong>${item.name}</strong>
|
| 794 |
+
${details}
|
|
|
|
| 795 |
</div>
|
| 796 |
<span class="cart-item-total">${itemTotal.toFixed(2)}</span>
|
| 797 |
<button class="cart-item-remove" onclick="removeFromCart('${item.id}')" title="${translations['remove_item_title']}">×</button>
|
|
|
|
| 805 |
|
| 806 |
function removeFromCart(itemId) {
|
| 807 |
cart = cart.filter(item => item.id !== itemId);
|
| 808 |
+
localStorage.setItem('esmiraCart', JSON.stringify(cart));
|
| 809 |
openCartModal();
|
| 810 |
updateCartButton();
|
| 811 |
}
|
|
|
|
| 813 |
function clearCart() {
|
| 814 |
if (confirm(translations['clear_cart_confirm'])) {
|
| 815 |
cart = [];
|
| 816 |
+
localStorage.removeItem('esmiraCart');
|
| 817 |
openCartModal();
|
| 818 |
updateCartButton();
|
| 819 |
}
|
|
|
|
| 835 |
.then(response => response.json())
|
| 836 |
.then(data => {
|
| 837 |
if (data.order_id) {
|
| 838 |
+
localStorage.removeItem('esmiraCart');
|
| 839 |
cart = [];
|
| 840 |
updateCartButton();
|
| 841 |
closeModal('cartModal');
|
|
|
|
| 854 |
function filterProducts() {
|
| 855 |
const searchTerm = document.getElementById('search-input').value.toLowerCase().trim();
|
| 856 |
const activeCategory = document.querySelector('.category-filter.active').dataset.category;
|
| 857 |
+
const activeType = document.querySelector('.type-filter.active').dataset.type;
|
|
|
|
| 858 |
const grid = document.getElementById('products-grid');
|
| 859 |
let visibleProducts = 0;
|
| 860 |
|
|
|
|
| 865 |
const name = productElement.dataset.name;
|
| 866 |
const description = productElement.dataset.description;
|
| 867 |
const category = productElement.dataset.category;
|
| 868 |
+
const type = productElement.dataset.type;
|
| 869 |
|
| 870 |
const matchesSearch = !searchTerm || name.includes(searchTerm) || description.includes(searchTerm);
|
| 871 |
const matchesCategory = activeCategory === 'all' || category === activeCategory;
|
| 872 |
+
const matchesType = activeType === 'all' || type === activeType;
|
| 873 |
|
| 874 |
+
if (matchesSearch && matchesCategory && matchesType) {
|
| 875 |
productElement.style.display = 'flex';
|
| 876 |
visibleProducts++;
|
| 877 |
} else {
|
|
|
|
| 896 |
filterProducts();
|
| 897 |
});
|
| 898 |
});
|
| 899 |
+
document.querySelectorAll('.type-filter').forEach(filter => {
|
| 900 |
filter.addEventListener('click', function() {
|
| 901 |
+
document.querySelector('.type-filter.active').classList.remove('active');
|
| 902 |
this.classList.add('active');
|
| 903 |
filterProducts();
|
| 904 |
});
|
|
|
|
| 928 |
if (event.target.classList.contains('modal')) {
|
| 929 |
closeModal(event.target.id);
|
| 930 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 931 |
});
|
| 932 |
|
| 933 |
window.addEventListener('keydown', function(event) {
|
|
|
|
| 971 |
</div>
|
| 972 |
|
| 973 |
<div style="font-size: 1rem; line-height: 1.7; padding: 0 10px;">
|
| 974 |
+
<p><strong>{{ _['product_type'] }}:</strong> {{ product.get('type') }}</p>
|
| 975 |
<p><strong>{{ _['category'] }}:</strong> {{ product.get('category', _['no_category']) }}</p>
|
|
|
|
| 976 |
{% if product.get('variants') and product.variants|length > 0 %}
|
| 977 |
<p style="font-size: 1.4rem; font-weight: bold; color: var(--primary-color); margin: 15px 0;">
|
| 978 |
{{ _['from_price'] }} {{ "%.2f"|format(product.variants|map(attribute='price')|min) }} {{ currency_code }}
|
|
|
|
| 987 |
{% endfor %}
|
| 988 |
</ul>
|
| 989 |
{% endif %}
|
| 990 |
+
{% if product.type == 'Одежда' and product.get('sizes') and product.sizes|length > 0 %}
|
| 991 |
+
<p style="margin-top: 15px;"><strong>{{ _['available_sizes'] }}</strong></p>
|
| 992 |
+
<ul style="list-style: none; padding-left: 0;">
|
| 993 |
+
{% for size in product.sizes %}
|
| 994 |
+
<li style="padding: 5px 0; border-bottom: 1px solid var(--border-color);">
|
| 995 |
+
- {{ size.name }}
|
| 996 |
+
</li>
|
| 997 |
+
{% endfor %}
|
| 998 |
+
</ul>
|
| 999 |
+
{% endif %}
|
| 1000 |
<p style="margin-top: 20px;"><strong>{{ _['description'] }}:</strong><br> {{ product.get('description', _['no_description'])|replace('\\n', '<br>')|safe }}</p>
|
| 1001 |
</div>
|
| 1002 |
<div style="padding: 20px 10px 10px; text-align: center;">
|
|
|
|
| 1013 |
<head>
|
| 1014 |
<meta charset="UTF-8">
|
| 1015 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 1016 |
+
<title>{{ _['order_page_title'] }}{{ order.id }} - Esmira</title>
|
| 1017 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 1018 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 1019 |
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@400;600;700&family=Georgia&display=swap" rel="stylesheet">
|
| 1020 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
| 1021 |
<style>
|
| 1022 |
:root {
|
| 1023 |
+
--primary-color: #B22222;
|
| 1024 |
+
--primary-dark: #8B0000;
|
| 1025 |
+
--surface-color: #2b2b2b;
|
| 1026 |
+
--background-color: #1c1c1c;
|
| 1027 |
--text-color: #F5F5F5;
|
| 1028 |
+
--text-color-muted: #a0a0a0;
|
| 1029 |
+
--border-color: #444;
|
| 1030 |
}
|
| 1031 |
body { font-family: 'Georgia', serif; background: var(--background-color); color: var(--text-color); line-height: 1.6; padding: 15px; }
|
| 1032 |
.container { max-width: 800px; margin: 20px auto; padding: 30px; background: var(--surface-color); border-radius: 16px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.6); border: 1px solid var(--border-color); }
|
|
|
|
| 1043 |
.order-summary { margin-top: 30px; padding-top: 20px; border-top: 2px solid var(--primary-color); text-align: right; }
|
| 1044 |
.order-summary p { margin-bottom: 10px; font-size: 1.1rem; }
|
| 1045 |
.order-summary strong { font-size: 1.5rem; color: var(--primary-color); }
|
| 1046 |
+
.customer-info { margin-top: 30px; background-color: rgba(178, 34, 34, 0.05); padding: 20px; border-radius: 12px; border: 1px solid var(--primary-color);}
|
| 1047 |
.customer-info p { margin-bottom: 8px; font-size: 1rem; }
|
| 1048 |
.customer-info strong { color: var(--text-color); }
|
| 1049 |
.actions { margin-top: 30px; text-align: center; }
|
|
|
|
| 1068 |
<div class="item-details">
|
| 1069 |
<strong>{{ item.name }}</strong>
|
| 1070 |
<span>{{ _['cart_item_variant'] }}: {{ item.variantName }}</span>
|
| 1071 |
+
{% if item.sizeName %}
|
| 1072 |
+
<span>{{ _['cart_item_size'] }}: {{ item.sizeName }}</span>
|
| 1073 |
+
{% endif %}
|
| 1074 |
<span>{{ "%.2f"|format(item.price) }} {{ currency_code }} × {{ item.quantity }}</span>
|
| 1075 |
</div>
|
| 1076 |
<div class="item-total">
|
|
|
|
| 1100 |
function sendOrderViaWhatsApp() {
|
| 1101 |
const orderId = '{{ order.id }}';
|
| 1102 |
const orderUrl = `{{ request.url }}`;
|
| 1103 |
+
const whatsappNumber = "{{ whatsapp_number }}";
|
| 1104 |
|
| 1105 |
let message = `{{ _['whatsapp_confirm_message_1'] }}%0A%0A`;
|
| 1106 |
message += `*{{ _['whatsapp_confirm_message_2'] }}* ${orderId}%0A`;
|
|
|
|
| 1128 |
<head>
|
| 1129 |
<meta charset="UTF-8">
|
| 1130 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 1131 |
+
<title>Админ-панель - Esmira</title>
|
| 1132 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 1133 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 1134 |
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@600&family=Georgia&display=swap" rel="stylesheet">
|
| 1135 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
| 1136 |
<style>
|
| 1137 |
:root {
|
| 1138 |
+
--primary-color: #B22222;
|
| 1139 |
+
--primary-dark: #8B0000;
|
| 1140 |
+
--surface-color: #2b2b2b;
|
| 1141 |
+
--background-color: #1c1c1c;
|
| 1142 |
--text-color: #F5F5F5;
|
| 1143 |
+
--text-color-muted: #a0a0a0;
|
| 1144 |
+
--border-color: #444;
|
| 1145 |
--success-bg: #113d21;
|
| 1146 |
--success-text: #6ee791;
|
| 1147 |
--error-bg: #4d0a0a;
|
|
|
|
| 1160 |
|
| 1161 |
label { font-weight: 500; margin-top: 12px; display: block; color: var(--text-color); font-size: 0.9rem;}
|
| 1162 |
input[type="text"], input[type="number"], input[type="password"], input[type="tel"], textarea, select { width: 100%; padding: 10px 12px; margin-top: 6px; border: 1px solid var(--border-color); border-radius: 6px; font-size: 0.95rem; box-sizing: border-box; transition: border-color 0.3s ease; background-color: var(--background-color); color: var(--text-color); }
|
| 1163 |
+
input:focus, textarea:focus, select:focus { border-color: var(--primary-color); outline: none; box-shadow: 0 0 0 3px rgba(178, 34, 34, 0.2); }
|
| 1164 |
textarea { min-height: 90px; resize: vertical; }
|
| 1165 |
input[type="file"] { padding: 8px; background-color: #222; cursor: pointer; border: 1px solid var(--border-color); border-radius: 6px; }
|
| 1166 |
input[type="checkbox"] { transform: scale(1.2); margin-right: 8px; vertical-align: middle; accent-color: var(--primary-color); }
|
|
|
|
| 1178 |
.photo-edit-item { position: relative; }
|
| 1179 |
.photo-edit-item input[type="checkbox"] { position: absolute; top: 0px; right: 0px; transform: scale(1.3); accent-color: #ff453a; cursor: pointer; }
|
| 1180 |
|
| 1181 |
+
.input-group { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
|
| 1182 |
+
.input-group input { flex-grow: 1; margin: 0; }
|
| 1183 |
+
.remove-btn { background-color: #8B0000; color: white; padding: 6px 10px; font-size: 0.8rem; margin: 0; border-radius: 6px; }
|
| 1184 |
+
.add-btn { background-color: #1c4532; color: var(--text-color); margin-top: 8px;}
|
| 1185 |
+
.add-btn:hover { background-color: #22543d; }
|
| 1186 |
|
| 1187 |
.message { padding: 12px 18px; border-radius: 8px; margin-bottom: 20px; font-size: 0.95rem; border: 1px solid;}
|
| 1188 |
.message.success { background-color: var(--success-bg); color: var(--success-text); border-color: var(--success-text);}
|
|
|
|
| 1210 |
<div class="container">
|
| 1211 |
<div class="header">
|
| 1212 |
<div class="logo-title-container" style="display: flex; align-items: center; gap: 15px;">
|
| 1213 |
+
<img src="https://huggingface.co/spaces/esmira-tkani/admin/resolve/main/Screenshot_20251225-134859.png" alt="Esmira Logo" style="height: 45px; width: 45px; border-radius: 50%;">
|
| 1214 |
+
<h1><i class="fas fa-tools"></i> Админ-панель Esmira</h1>
|
| 1215 |
</div>
|
| 1216 |
<a href="{{ url_for('catalog', lang_code='ru') }}" class="button"><i class="fas fa-store"></i> Перейти в каталог</a>
|
| 1217 |
</div>
|
|
|
|
| 1225 |
{% endif %}
|
| 1226 |
{% endwith %}
|
| 1227 |
|
| 1228 |
+
<div class="section">
|
| 1229 |
+
<h2><i class="fas fa-cogs"></i> Настройки</h2>
|
| 1230 |
+
<form method="POST">
|
| 1231 |
+
<input type="hidden" name="action" value="update_settings">
|
| 1232 |
+
<label for="whatsapp_number">Номер WhatsApp для заказов:</label>
|
| 1233 |
+
<input type="tel" id="whatsapp_number" name="whatsapp_number" value="{{ settings.get('whatsapp_number', '') }}" placeholder="77073363943">
|
| 1234 |
+
<button type="submit"><i class="fas fa-save"></i> Сохранить настройки</button>
|
| 1235 |
+
</form>
|
| 1236 |
+
</div>
|
| 1237 |
+
|
| 1238 |
+
|
| 1239 |
<div class="section">
|
| 1240 |
<h2><i class="fas fa-history"></i> История заказов</h2>
|
| 1241 |
{% if orders %}
|
|
|
|
| 1275 |
{% endif %}
|
| 1276 |
</div>
|
| 1277 |
|
| 1278 |
+
<div class="section">
|
| 1279 |
+
<h2><i class="fas fa-tags"></i> Категории</h2>
|
| 1280 |
+
<details>
|
| 1281 |
+
<summary><i class="fas fa-plus-circle"></i> Добавить новую категорию</summary>
|
| 1282 |
+
<div style="padding: 15px;">
|
| 1283 |
+
<form method="POST">
|
| 1284 |
+
<input type="hidden" name="action" value="add_category">
|
| 1285 |
+
<label for="add_category_name">Название новой категории:</label>
|
| 1286 |
+
<input type="text" id="add_category_name" name="category_name" required>
|
| 1287 |
+
<button type="submit"><i class="fas fa-plus"></i> Добавить</button>
|
| 1288 |
+
</form>
|
| 1289 |
+
</div>
|
| 1290 |
+
</details>
|
| 1291 |
+
<h3>Существующие категории:</h3>
|
| 1292 |
+
{% if categories %}
|
| 1293 |
+
<div class="item-list">
|
| 1294 |
+
{% for category in categories %}
|
| 1295 |
+
<div class="item" style="display: flex; justify-content: space-between; align-items: center; padding: 10px;">
|
| 1296 |
+
<span>{{ category }}</span>
|
| 1297 |
+
<form method="POST" style="margin: 0;" onsubmit="return confirm('Удалить категорию \'{{ category }}\'?');">
|
| 1298 |
+
<input type="hidden" name="action" value="delete_category">
|
| 1299 |
+
<input type="hidden" name="category_name" value="{{ category }}">
|
| 1300 |
+
<button type="submit" class="delete-button" style="padding: 6px 12px; font-size: 0.8rem; margin: 0;"><i class="fas fa-trash-alt"></i></button>
|
| 1301 |
+
</form>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1302 |
</div>
|
| 1303 |
+
{% endfor %}
|
| 1304 |
</div>
|
| 1305 |
+
{% else %}
|
| 1306 |
+
<p>Категорий пока нет.</p>
|
| 1307 |
+
{% endif %}
|
| 1308 |
</div>
|
| 1309 |
|
| 1310 |
+
|
| 1311 |
<div class="section">
|
| 1312 |
<h2><i class="fas fa-box-open"></i> Товары</h2>
|
| 1313 |
<details>
|
| 1314 |
<summary><i class="fas fa-plus-circle"></i> Добавить новый товар</summary>
|
| 1315 |
<div style="padding: 15px;">
|
| 1316 |
+
<form method="POST" enctype="multipart/form-data" id="addProductForm">
|
| 1317 |
<input type="hidden" name="action" value="add_product">
|
| 1318 |
<label for="add_name">Название товара *:</label>
|
| 1319 |
<input type="text" id="add_name" name="name" required>
|
| 1320 |
+
|
| 1321 |
+
<label for="add_type">Тип товара *:</label>
|
| 1322 |
+
<select id="add_type" name="type" required onchange="toggleSizeInputs('add')">
|
| 1323 |
+
<option value="Ткани">Ткани</option>
|
| 1324 |
+
<option value="Одежда">Одежда</option>
|
| 1325 |
+
</select>
|
| 1326 |
+
|
| 1327 |
<label for="add_description">Описание:</label>
|
| 1328 |
<textarea id="add_description" name="description" rows="4"></textarea>
|
| 1329 |
|
|
|
|
| 1335 |
{% endfor %}
|
| 1336 |
</select>
|
| 1337 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1338 |
<label for="add_photos">Фотографии (до 10 шт.):</label>
|
| 1339 |
<input type="file" id="add_photos" name="photos" accept="image/*" multiple>
|
| 1340 |
|
| 1341 |
<h4>Варианты и цены *:</h4>
|
| 1342 |
<div id="add-variants-container">
|
| 1343 |
+
<div class="input-group">
|
| 1344 |
+
<input type="text" name="variant_names" placeholder="Название варианта (напр. Красный)" required>
|
| 1345 |
<input type="number" name="variant_prices" step="0.01" min="0" placeholder="Цена" required>
|
| 1346 |
+
<button type="button" class="remove-btn" onclick="removeInputGroup(this)"><i class="fas fa-times"></i></button>
|
| 1347 |
</div>
|
| 1348 |
</div>
|
| 1349 |
+
<button type="button" class="button add-btn" onclick="addVariantInput('add-variants-container')"><i class="fas fa-plus"></i> Добавить вариант</button>
|
| 1350 |
+
|
| 1351 |
+
<div id="add-sizes-section" style="display: none;">
|
| 1352 |
+
<h4>Р��змеры *:</h4>
|
| 1353 |
+
<div id="add-sizes-container">
|
| 1354 |
+
<div class="input-group">
|
| 1355 |
+
<input type="text" name="size_names" placeholder="Название размера (напр. S, M, 42)">
|
| 1356 |
+
<button type="button" class="remove-btn" onclick="removeInputGroup(this, false)"><i class="fas fa-times"></i></button>
|
| 1357 |
+
</div>
|
| 1358 |
+
</div>
|
| 1359 |
+
<button type="button" class="button add-btn" onclick="addSizeInput('add-sizes-container')"><i class="fas fa-plus"></i> Добавить размер</button>
|
| 1360 |
+
</div>
|
| 1361 |
|
| 1362 |
<div style="margin-top: 20px;">
|
| 1363 |
<input type="checkbox" id="add_is_top" name="is_top">
|
|
|
|
| 1389 |
{{ product['name'] }}
|
| 1390 |
{% if product.get('is_top', False) %}<span class="status-indicator top-product"><i class="fas fa-star fa-xs"></i> Топ</span>{% endif %}
|
| 1391 |
</h3>
|
| 1392 |
+
<p style="font-size: 0.9rem; color: var(--text-color-muted);"><strong>Тип:</strong> {{ product.get('type') }}</p>
|
| 1393 |
<p style="font-size: 0.9rem; color: var(--text-color-muted);"><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
|
|
|
|
| 1394 |
</div>
|
| 1395 |
</div>
|
| 1396 |
|
|
|
|
| 1410 |
<input type="hidden" name="product_id" value="{{ product.id }}">
|
| 1411 |
<label>Название *:</label>
|
| 1412 |
<input type="text" name="name" value="{{ product['name'] }}" required>
|
| 1413 |
+
|
| 1414 |
+
<label>Тип товара *:</label>
|
| 1415 |
+
<select name="type" required onchange="toggleSizeInputs('edit-{{ product.id }}')">
|
| 1416 |
+
<option value="Ткани" {% if product.get('type') == 'Ткани' %}selected{% endif %}>Ткани</option>
|
| 1417 |
+
<option value="Одежда" {% if product.get('type') == 'Одежда' %}selected{% endif %}>Одежда</option>
|
| 1418 |
+
</select>
|
| 1419 |
+
|
| 1420 |
<label>Описание:</label>
|
| 1421 |
<textarea name="description" rows="4">{{ product.get('description', '') }}</textarea>
|
| 1422 |
|
|
|
|
| 1427 |
<option value="{{ category }}" {% if product.get('category') == category %}selected{% endif %}>{{ category }}</option>
|
| 1428 |
{% endfor %}
|
| 1429 |
</select>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1430 |
|
| 1431 |
<label>Текущие фотографии (отметьте для удаления):</label>
|
| 1432 |
<div class="photo-preview-edit">
|
|
|
|
| 1448 |
<h4>Варианты и цены *:</h4>
|
| 1449 |
<div id="edit-variants-container-{{ product.id }}">
|
| 1450 |
{% for variant in product.get('variants', []) %}
|
| 1451 |
+
<div class="input-group">
|
| 1452 |
<input type="text" name="variant_names" value="{{ variant.name }}" required>
|
| 1453 |
<input type="number" name="variant_prices" step="0.01" min="0" value="{{ variant.price }}" required>
|
| 1454 |
+
<button type="button" class="remove-btn" onclick="removeInputGroup(this)"><i class="fas fa-times"></i></button>
|
| 1455 |
</div>
|
| 1456 |
{% endfor %}
|
| 1457 |
</div>
|
| 1458 |
+
<button type="button" class="button add-btn" onclick="addVariantInput('edit-variants-container-{{ product.id }}')"><i class="fas fa-plus"></i> Добавить вариант</button>
|
| 1459 |
+
|
| 1460 |
+
<div id="edit-{{ product.id }}-sizes-section" style="display: {% if product.type == 'Одежда' %}block{% else %}none{% endif %};">
|
| 1461 |
+
<h4>Размеры *:</h4>
|
| 1462 |
+
<div id="edit-sizes-container-{{ product.id }}">
|
| 1463 |
+
{% for size in product.get('sizes', []) %}
|
| 1464 |
+
<div class="input-group">
|
| 1465 |
+
<input type="text" name="size_names" value="{{ size.name }}">
|
| 1466 |
+
<button type="button" class="remove-btn" onclick="removeInputGroup(this, false)"><i class="fas fa-times"></i></button>
|
| 1467 |
+
</div>
|
| 1468 |
+
{% endfor %}
|
| 1469 |
+
{% if product.get('sizes', []) | length == 0 %}
|
| 1470 |
+
<div class="input-group">
|
| 1471 |
+
<input type="text" name="size_names" placeholder="Название размера (напр. S, M, 42)">
|
| 1472 |
+
<button type="button" class="remove-btn" onclick="removeInputGroup(this, false)"><i class="fas fa-times"></i></button>
|
| 1473 |
+
</div>
|
| 1474 |
+
{% endif %}
|
| 1475 |
+
</div>
|
| 1476 |
+
<button type="button" class="button add-btn" onclick="addSizeInput('edit-sizes-container-{{ product.id }}')"><i class="fas fa-plus"></i> Добавить размер</button>
|
| 1477 |
+
</div>
|
| 1478 |
|
| 1479 |
<div style="margin-top: 20px;">
|
| 1480 |
<input type="checkbox" id="edit_is_top_{{ product.id }}" name="is_top" {% if product.get('is_top', False) %}checked{% endif %}>
|
|
|
|
| 1508 |
const form = document.getElementById(formId);
|
| 1509 |
form.style.display = form.style.display === 'none' ? 'block' : 'none';
|
| 1510 |
}
|
| 1511 |
+
|
| 1512 |
+
function toggleSizeInputs(formPrefix) {
|
| 1513 |
+
const typeSelect = document.querySelector(`#${formPrefix}_type, #edit-form-${formPrefix.split('-')[1]} select[name='type']`);
|
| 1514 |
+
const sizesSection = document.getElementById(`${formPrefix}-sizes-section`);
|
| 1515 |
+
if (typeSelect && sizesSection) {
|
| 1516 |
+
sizesSection.style.display = typeSelect.value === 'Одежда' ? 'block' : 'none';
|
| 1517 |
+
}
|
| 1518 |
+
}
|
| 1519 |
+
|
| 1520 |
|
| 1521 |
function addVariantInput(containerId) {
|
| 1522 |
const container = document.getElementById(containerId);
|
| 1523 |
const newInputGroup = document.createElement('div');
|
| 1524 |
+
newInputGroup.className = 'input-group';
|
| 1525 |
newInputGroup.innerHTML = `
|
| 1526 |
<input type="text" name="variant_names" placeholder="Название варианта" required>
|
| 1527 |
<input type="number" name="variant_prices" step="0.01" min="0" placeholder="Цена" required>
|
| 1528 |
+
<button type="button" class="remove-btn" onclick="removeInputGroup(this)"><i class="fas fa-times"></i></button>
|
| 1529 |
+
`;
|
| 1530 |
+
container.appendChild(newInputGroup);
|
| 1531 |
+
}
|
| 1532 |
+
|
| 1533 |
+
function addSizeInput(containerId) {
|
| 1534 |
+
const container = document.getElementById(containerId);
|
| 1535 |
+
const newInputGroup = document.createElement('div');
|
| 1536 |
+
newInputGroup.className = 'input-group';
|
| 1537 |
+
newInputGroup.innerHTML = `
|
| 1538 |
+
<input type="text" name="size_names" placeholder="Название размера">
|
| 1539 |
+
<button type="button" class="remove-btn" onclick="removeInputGroup(this, false)"><i class="fas fa-times"></i></button>
|
| 1540 |
`;
|
| 1541 |
container.appendChild(newInputGroup);
|
| 1542 |
}
|
| 1543 |
|
| 1544 |
+
function removeInputGroup(button, requireOne = true) {
|
| 1545 |
+
const group = button.closest('.input-group');
|
| 1546 |
const container = group.parentNode;
|
| 1547 |
+
if (!requireOne || container.children.length > 1) {
|
| 1548 |
group.remove();
|
| 1549 |
} else {
|
| 1550 |
alert("Должен быть хотя бы один вариант.");
|
|
|
|
| 1574 |
data = load_data()
|
| 1575 |
all_products = data.get('products', [])
|
| 1576 |
categories = sorted(data.get('categories', []))
|
| 1577 |
+
settings = data.get('settings', {})
|
| 1578 |
|
| 1579 |
products_in_stock = [p for p in all_products if p.get('variants')]
|
| 1580 |
|
|
|
|
| 1587 |
CATALOG_TEMPLATE,
|
| 1588 |
products=products_sorted,
|
| 1589 |
categories=categories,
|
|
|
|
| 1590 |
repo_id=REPO_ID,
|
| 1591 |
store_addresses=STORE_ADDRESSES,
|
| 1592 |
currency_code=CURRENCY_CODE,
|
| 1593 |
lang_code=g.lang_code,
|
| 1594 |
+
_=g.translations,
|
| 1595 |
+
settings=settings
|
| 1596 |
)
|
| 1597 |
|
| 1598 |
@app.route('/<lang_code>/product/<int:index>')
|
|
|
|
| 1643 |
|
| 1644 |
try:
|
| 1645 |
price = float(item['price'])
|
| 1646 |
+
quantity = float(item['quantity'])
|
| 1647 |
if price < 0 or quantity <= 0: raise ValueError("Invalid price/qty")
|
| 1648 |
|
| 1649 |
total_price += price * quantity
|
|
|
|
| 1655 |
"price": price,
|
| 1656 |
"quantity": quantity,
|
| 1657 |
"variantName": item.get('variantName', 'N/A'),
|
| 1658 |
+
"sizeName": item.get('sizeName'),
|
| 1659 |
"photo": photo_name,
|
| 1660 |
"photo_url": photo_url
|
| 1661 |
})
|
|
|
|
| 1686 |
def view_order(lang_code, order_id):
|
| 1687 |
data = load_data()
|
| 1688 |
order = data.get('orders', {}).get(order_id)
|
| 1689 |
+
settings = data.get('settings', {})
|
| 1690 |
+
whatsapp_number = settings.get('whatsapp_number', '77073363943')
|
| 1691 |
status_map = STATUS_MAPS.get(lang_code, STATUS_MAPS['ru'])
|
| 1692 |
+
return render_template_string(ORDER_TEMPLATE, order=order, status_map=status_map, currency_code=CURRENCY_CODE, lang_code=g.lang_code, _=g.translations, whatsapp_number=whatsapp_number)
|
| 1693 |
|
| 1694 |
@app.route('/admin', methods=['GET', 'POST'])
|
| 1695 |
def admin():
|
| 1696 |
data = load_data()
|
| 1697 |
if 'orders' not in data: data['orders'] = {}
|
| 1698 |
+
if 'settings' not in data: data['settings'] = {}
|
| 1699 |
|
| 1700 |
needs_save = False
|
| 1701 |
for p in data.get('products', []):
|
|
|
|
| 1708 |
if request.method == 'POST':
|
| 1709 |
action = request.form.get('action')
|
| 1710 |
try:
|
| 1711 |
+
if action == 'update_settings':
|
| 1712 |
+
whatsapp_number = request.form.get('whatsapp_number', '').strip().replace('+', '')
|
| 1713 |
+
data['settings']['whatsapp_number'] = whatsapp_number
|
| 1714 |
+
save_data(data)
|
| 1715 |
+
flash('Настройки сохранены.', 'success')
|
| 1716 |
+
|
| 1717 |
+
elif action == 'update_order_status':
|
| 1718 |
order_id = request.form.get('order_id')
|
| 1719 |
new_status = request.form.get('new_status')
|
| 1720 |
if order_id in data['orders'] and new_status in STATUS_MAPS['ru']:
|
|
|
|
| 1745 |
else:
|
| 1746 |
flash("Категория не найдена.", 'error')
|
| 1747 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1748 |
elif action == 'add_product' or action == 'edit_product':
|
| 1749 |
name = request.form.get('name', '').strip()
|
| 1750 |
+
product_type = request.form.get('type')
|
| 1751 |
+
|
| 1752 |
+
if not name or not product_type:
|
| 1753 |
+
flash("Название и тип товара обязательны.", 'error')
|
| 1754 |
return redirect(url_for('admin'))
|
| 1755 |
|
| 1756 |
variant_names = [v.strip() for v in request.form.getlist('variant_names') if v.strip()]
|
|
|
|
| 1770 |
flash("Неверный формат цены в вариантах.", 'error')
|
| 1771 |
return redirect(url_for('admin'))
|
| 1772 |
|
| 1773 |
+
sizes = []
|
| 1774 |
+
if product_type == 'Одежда':
|
| 1775 |
+
size_names = [s.strip() for s in request.form.getlist('size_names') if s.strip()]
|
| 1776 |
+
if not size_names:
|
| 1777 |
+
flash("Для одежды необходимо указать хотя бы один размер.", 'error')
|
| 1778 |
+
return redirect(url_for('admin'))
|
| 1779 |
+
sizes = [{'name': s_name} for s_name in size_names]
|
| 1780 |
+
|
| 1781 |
product_data = {
|
| 1782 |
'name': name,
|
| 1783 |
+
'type': product_type,
|
| 1784 |
'description': request.form.get('description', '').strip(),
|
| 1785 |
'category': request.form.get('category'),
|
|
|
|
| 1786 |
'is_top': 'is_top' in request.form,
|
| 1787 |
+
'variants': variants,
|
| 1788 |
+
'sizes': sizes if product_type == 'Одежда' else []
|
| 1789 |
}
|
| 1790 |
|
| 1791 |
newly_uploaded_photos = []
|
|
|
|
| 1883 |
ADMIN_TEMPLATE,
|
| 1884 |
products=sorted(current_data.get('products', []), key=lambda p: p.get('name', '').lower()),
|
| 1885 |
categories=sorted(current_data.get('categories', [])),
|
|
|
|
| 1886 |
orders=list(current_data.get('orders', {}).values()),
|
| 1887 |
+
settings=current_data.get('settings', {}),
|
| 1888 |
status_map_ru=STATUS_MAPS['ru'],
|
| 1889 |
repo_id=REPO_ID,
|
| 1890 |
store_addresses=STORE_ADDRESSES,
|