Kgshop commited on
Commit
eab79ff
·
verified ·
1 Parent(s): b541cf3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +325 -271
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': "SHAIK парфюм оптом и в розницу - Каталог",
59
- 'header_title': "SHAIK Parfum",
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
- 'quantity_label': "Количество:",
 
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
- 'brand': "Бренд",
 
 
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': "Здравствуйте! Хочу подтвердить свой заказ на SHAIK парфюм:",
104
  'whatsapp_confirm_message_2': "Номер заказа:",
105
  'whatsapp_confirm_message_3': "Ссылка на заказ:",
106
  'whatsapp_confirm_message_4': "Пожалуйста, свяжитесь со мной для уточнения деталей.",
107
  },
108
  'kz': {
109
- 'page_title': "SHAIK парфюмериясы көтерме және бөлшек саудада - Каталог",
110
- 'header_title': "SHAIK Parfum",
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
- 'quantity_label': "Саны:",
 
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
- 'brand': "Бренд",
 
 
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': "Сәлеметсіз бе! SHAIK парфюмериясына берген тапсырысымды растағым келеді:",
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': [], 'brands': [], 'orders': {}}, f)
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': [], 'brands': [], 'orders': {}}
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: #FF3B30;
353
- --primary-dark: #D9322A;
354
- --surface-color: #1A1A1A;
355
- --background-color: #121212;
356
  --text-color: #F5F5F5;
357
- --text-color-muted: #999;
358
- --border-color: #333333;
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(18, 18, 18, 0.8); backdrop-filter: blur(10px); border-bottom: 1px solid var(--border-color); position: sticky; top: 0; z-index: 1000; }
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; box-shadow: 0 0 10px rgba(255, 59, 48, 0.5); }
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(255, 59, 48, 0.2); }
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
- .category-filter, .brand-filter { 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; }
394
- .category-filter.active, .brand-filter.active, .category-filter:hover, .brand-filter:hover { background-color: var(--primary-color); color: #fff; border-color: var(--primary-color); font-weight: 500; box-shadow: 0 2px 8px rgba(255, 59, 48, 0.3); transform: translateY(-2px); }
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(255, 59, 48, 0.15); border-color: var(--primary-color); }
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(255, 59, 48, 0.4); }
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(255, 59, 48, 0.4); z-index: 1000; transition: transform 0.2s ease; }
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, #FF3B30, #D9322A); 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); }
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/Shaik-parfume/app/resolve/main/icon.png" alt="SHAIK Logo">
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">{{ _['category'] }}:</span>
490
  <div class="filters-container">
491
- <button class="category-filter active" data-category="all">{{ _['all_filter'] }}</button>
492
- {% for category in categories %}
493
- <button class="category-filter" data-category="{{ category }}">{{ category }}</button>
494
- {% endfor %}
495
  </div>
496
  </div>
497
- {% if brands %}
498
  <div>
499
- <span class="filter-label">{{ _['brand'] }}:</span>
500
  <div class="filters-container">
501
- <button class="brand-filter active" data-brand="all">{{ _['all_filter'] }}</button>
502
- {% for brand in brands %}
503
- <button class="brand-filter" data-brand="{{ brand }}">{{ brand }}</button>
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-brand="{{ product.get('brand', _['no_brand']) }}">
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="Закрыть">&times;</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
- <button id="whatsapp-fab" class="fab" onclick="toggleWhatsAppModal()"><i class="fab fa-whatsapp"></i></button>
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('shaikCart') || '[]');
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 setupVariantSelect(variants) {
667
- const select = document.getElementById('variantSelect');
668
  select.innerHTML = '';
669
- if (variants && variants.length > 0) {
670
- variants.forEach((variant, index) => {
671
- const option = document.createElement('option');
672
- option.value = index;
673
- option.text = `${variant.name} - ${variant.price.toFixed(2)} ${currencyCode}`;
674
- select.appendChild(option);
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
- setupVariantSelect(product.variants);
688
- document.getElementById('quantityInput').value = 1;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 quantity = parseInt(document.getElementById('quantityInput').value);
697
- const variantIndex = document.getElementById('variantSelect').value;
698
  const product = products[selectedProductIndex];
699
-
700
- if (!product || !product.variants || product.variants.length === 0) {
701
- alert("Ошибка: Варианты для этого товара не найдены.");
 
 
702
  return;
703
  }
704
 
 
705
  const selectedVariant = product.variants[variantIndex];
 
 
 
 
 
 
 
 
 
 
 
 
 
706
 
707
- if (isNaN(quantity) || quantity <= 0) {
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('shaikCart', JSON.stringify(cart));
730
  closeModal('quantityModal');
731
  updateCartButton();
732
- showNotification(`${product.name} (${selectedVariant.name}) ${translations['add_to_cart_notification']}`);
733
  }
734
 
735
  function updateCartButton() {
736
  const cartCountElement = document.getElementById('cart-count');
737
  const cartButton = document.getElementById('cart-button');
738
- const totalItems = cart.reduce((sum, item) => sum + item.quantity, 0);
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
- <p class="cart-item-price">${translations['cart_item_variant']}: ${item.variantName}</p>
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']}">&times;</button>
@@ -779,7 +805,7 @@ CATALOG_TEMPLATE = '''
779
 
780
  function removeFromCart(itemId) {
781
  cart = cart.filter(item => item.id !== itemId);
782
- localStorage.setItem('shaikCart', JSON.stringify(cart));
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('shaikCart');
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('shaikCart');
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 activeBrandEl = document.querySelector('.brand-filter.active');
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 brand = productElement.dataset.brand;
844
 
845
  const matchesSearch = !searchTerm || name.includes(searchTerm) || description.includes(searchTerm);
846
  const matchesCategory = activeCategory === 'all' || category === activeCategory;
847
- const matchesBrand = activeBrand === 'all' || brand === activeBrand;
848
 
849
- if (matchesSearch && matchesCategory && matchesBrand) {
850
  productElement.style.display = 'flex';
851
  visibleProducts++;
852
  } else {
@@ -871,9 +896,9 @@ CATALOG_TEMPLATE = '''
871
  filterProducts();
872
  });
873
  });
874
- document.querySelectorAll('.brand-filter').forEach(filter => {
875
  filter.addEventListener('click', function() {
876
- document.querySelector('.brand-filter.active').classList.remove('active');
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 }} - SHAIK парфюм</title>
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: #FF3B30;
994
- --primary-dark: #D9322A;
995
- --surface-color: #1A1A1A;
996
- --background-color: #121212;
997
  --text-color: #F5F5F5;
998
- --text-color-muted: #999;
999
- --border-color: #333333;
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(255, 59, 48, 0.05); padding: 20px; border-radius: 12px; border: 1px solid var(--primary-color);}
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 = "77762021169";
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>Админ-панель - SHAIK парфюм</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: #FF3B30;
1106
- --primary-dark: #D9322A;
1107
- --surface-color: #1A1A1A;
1108
- --background-color: #121212;
1109
  --text-color: #F5F5F5;
1110
- --text-color-muted: #999;
1111
- --border-color: #333333;
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(255, 59, 48, 0.2); }
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
- .variant-input-group { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
1149
- .variant-input-group input { flex-grow: 1; margin: 0; }
1150
- .remove-variant-btn { background-color: #8B0000; color: white; padding: 6px 10px; font-size: 0.8rem; margin: 0; border-radius: 6px; }
1151
- .add-variant-btn { background-color: #1c4532; color: var(--text-color); margin-top: 8px;}
1152
- .add-variant-btn:hover { background-color: #22543d; }
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/Shaik-parfume/app/resolve/main/icon.png" alt="SHAIK Logo" style="height: 45px; width: 45px; border-radius: 50%;">
1181
- <h1><i class="fas fa-tools"></i> Админ-панель</h1>
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
- <div class="flex-container">
1236
- <div class="flex-item">
1237
- <div class="section">
1238
- <h2><i class="fas fa-tags"></i> Категории</h2>
1239
- <details>
1240
- <summary><i class="fas fa-plus-circle"></i> Добавить новую категорию</summary>
1241
- <div style="padding: 15px;">
1242
- <form method="POST">
1243
- <input type="hidden" name="action" value="add_category">
1244
- <label for="add_category_name">Название новой категории:</label>
1245
- <input type="text" id="add_category_name" name="category_name" required>
1246
- <button type="submit"><i class="fas fa-plus"></i> Добавить</button>
1247
- </form>
1248
- </div>
1249
- </details>
1250
- <h3>Существующие категории:</h3>
1251
- {% if categories %}
1252
- <div class="item-list">
1253
- {% for category in categories %}
1254
- <div class="item" style="display: flex; justify-content: space-between; align-items: center; padding: 10px;">
1255
- <span>{{ category }}</span>
1256
- <form method="POST" style="margin: 0;" onsubmit="return confirm('Удалить категорию \'{{ category }}\'?');">
1257
- <input type="hidden" name="action" value="delete_category">
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="variant-input-group">
1339
- <input type="text" name="variant_names" placeholder="Название варианта (напр. 100ml)" required>
1340
  <input type="number" name="variant_prices" step="0.01" min="0" placeholder="Цена" required>
1341
- <button type="button" class="remove-variant-btn" onclick="removeVariantInput(this)"><i class="fas fa-times"></i></button>
1342
  </div>
1343
  </div>
1344
- <button type="button" class="button add-variant-btn" onclick="addVariantInput('add-variants-container')"><i class="fas fa-plus"></i> Добавить вариант</button>
 
 
 
 
 
 
 
 
 
 
 
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="variant-input-group">
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-variant-btn" onclick="removeVariantInput(this)"><i class="fas fa-times"></i></button>
1440
  </div>
1441
  {% endfor %}
1442
  </div>
1443
- <button type="button" class="button add-variant-btn" onclick="addVariantInput('edit-variants-container-{{ product.id }}')"><i class="fas fa-plus"></i> Добавить вариант</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 = 'variant-input-group';
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-variant-btn" onclick="removeVariantInput(this)"><i class="fas fa-times"></i></button>
 
 
 
 
 
 
 
 
 
 
 
1486
  `;
1487
  container.appendChild(newInputGroup);
1488
  }
1489
 
1490
- function removeVariantInput(button) {
1491
- const group = button.closest('.variant-input-group');
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
- brands = sorted(data.get('brands', []))
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 = int(item['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 == 'update_order_status':
 
 
 
 
 
 
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
- if not name:
1708
- flash("Название товара обязательно.", 'error')
 
 
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="Закрыть">&times;</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']}">&times;</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,