Kgshop commited on
Commit
c05c6b5
·
verified ·
1 Parent(s): cd63945

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +352 -103
app.py CHANGED
@@ -1,4 +1,5 @@
1
- from flask import Flask, render_template_string, request, redirect, url_for, send_file, flash, jsonify
 
2
  import json
3
  import os
4
  import logging
@@ -37,13 +38,127 @@ CURRENCY_NAME = 'Казахстанский тенге'
37
  DOWNLOAD_RETRIES = 3
38
  DOWNLOAD_DELAY = 5
39
 
40
- STATUS_MAP_RU = {
41
- "new": "Новый",
42
- "accepted": "Принят",
43
- "prepared": "Собран",
44
- "shipped": "Отправлен"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  }
46
 
 
47
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
48
 
49
 
@@ -86,7 +201,7 @@ def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWN
86
  try:
87
  if file_name == DATA_FILE:
88
  with open(file_name, 'w', encoding='utf-8') as f:
89
- json.dump({'products': [], 'categories': [], 'orders': {}}, f)
90
  logging.info(f"Created empty local file {file_name} because it was not found on HF.")
91
  except Exception as create_e:
92
  logging.error(f"Failed to create empty local file {file_name}: {create_e}")
@@ -150,7 +265,7 @@ def periodic_backup():
150
 
151
 
152
  def load_data():
153
- default_data = {'products': [], 'categories': [], 'orders': {}}
154
  try:
155
  with open(DATA_FILE, 'r', encoding='utf-8') as file:
156
  data = json.load(file)
@@ -160,6 +275,7 @@ def load_data():
160
  raise FileNotFoundError
161
  if 'products' not in data: data['products'] = []
162
  if 'categories' not in data: data['categories'] = []
 
163
  if 'orders' not in data: data['orders'] = {}
164
  return data
165
  except FileNotFoundError:
@@ -177,6 +293,7 @@ def load_data():
177
  return default_data
178
  if 'products' not in data: data['products'] = []
179
  if 'categories' not in data: data['categories'] = []
 
180
  if 'orders' not in data: data['orders'] = {}
181
  return data
182
  except FileNotFoundError:
@@ -206,6 +323,7 @@ def save_data(data):
206
  return
207
  if 'products' not in data: data['products'] = []
208
  if 'categories' not in data: data['categories'] = []
 
209
  if 'orders' not in data: data['orders'] = {}
210
 
211
  with open(DATA_FILE, 'w', encoding='utf-8') as file:
@@ -219,11 +337,11 @@ def save_data(data):
219
 
220
  CATALOG_TEMPLATE = '''
221
  <!DOCTYPE html>
222
- <html lang="ru">
223
  <head>
224
  <meta charset="UTF-8">
225
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
226
- <title>SHAIK парфюм оптом и в розницу - Каталог</title>
227
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
228
  <link rel="preconnect" href="https://fonts.googleapis.com">
229
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
@@ -256,6 +374,10 @@ CATALOG_TEMPLATE = '''
256
  .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); }
257
  .header h1 { font-family: 'Cormorant Garamond', serif; font-size: 1.8rem; font-weight: 700; color: var(--text-color); }
258
 
 
 
 
 
259
  .store-addresses { padding: 20px; text-align: center; background-color: var(--surface-color); border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.5); font-size: 0.95rem; color: var(--text-color-muted); margin: 20px; }
260
  .store-addresses h3 { font-family: 'Cormorant Garamond', serif; color: var(--primary-color); font-size: 1.3rem; margin-bottom: 10px; font-weight: 600; display: flex; align-items: center; justify-content: center; gap: 8px; }
261
  .store-addresses p { margin: 5px 0; }
@@ -264,10 +386,12 @@ CATALOG_TEMPLATE = '''
264
  #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); }
265
  #search-input:focus { border-color: var(--primary-color); box-shadow: 0 0 0 4px rgba(255, 59, 48, 0.2); }
266
 
267
- .filters-container { margin: 0 20px 20px; display: flex; overflow-x: auto; gap: 10px; padding-bottom: 10px; scrollbar-width: none; -ms-overflow-style: none; }
 
268
  .filters-container::-webkit-scrollbar { display: none; }
269
- .category-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; }
270
- .category-filter.active, .category-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); }
 
271
 
272
  .products-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 15px; padding: 0 20px 120px; }
273
  @media (min-width: 600px) { .products-grid { grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 20px; } }
@@ -340,12 +464,16 @@ CATALOG_TEMPLATE = '''
340
  <header class="header">
341
  <div class="logo-title-container">
342
  <img src="https://huggingface.co/spaces/Shaik-parfume/app/resolve/main/icon.png" alt="SHAIK Logo">
343
- <h1>SHAIK Parfum</h1>
 
 
 
 
344
  </div>
345
  </header>
346
 
347
  <div class="store-addresses">
348
- <h3><i class="fas fa-map-marker-alt"></i> Наши адреса в г. Алматы</h3>
349
  {% for address in store_addresses %}
350
  <p>{{ address }}</p>
351
  {% endfor %}
@@ -353,24 +481,42 @@ CATALOG_TEMPLATE = '''
353
 
354
 
355
  <div class="search-container">
356
- <input type="text" id="search-input" placeholder="Поиск по названию или описанию...">
357
  </div>
358
 
359
- <div class="filters-container">
360
- <button class="category-filter active" data-category="all">Все</button>
361
- {% for category in categories %}
362
- <button class="category-filter" data-category="{{ category }}">{{ category }}</button>
363
- {% endfor %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
364
  </div>
365
 
 
366
  <main class="products-grid" id="products-grid">
367
  {% for product in products %}
368
  <div class="product"
369
  data-name="{{ product['name']|lower }}"
370
  data-description="{{ product.get('description', '')|lower }}"
371
- data-category="{{ product.get('category', 'Без категории') }}">
 
372
  {% if product.get('is_top', False) %}
373
- <span class="top-product-indicator"><i class="fas fa-star fa-xs"></i> Топ</span>
374
  {% endif %}
375
  <div class="product-image" onclick="openModal({{ loop.index0 }})" style="cursor: pointer;">
376
  {% if product.get('photos') and product['photos']|length > 0 %}
@@ -385,7 +531,7 @@ CATALOG_TEMPLATE = '''
385
  <h2>{{ product['name'] }}</h2>
386
  <div class="product-price">
387
  {% if product.get('variants') and product.variants|length > 0 %}
388
- <span class="from-text">от</span> {{ "%.2f"|format(product.variants|map(attribute='price')|min) }} {{ currency_code }}
389
  {% else %}
390
  -
391
  {% endif %}
@@ -393,13 +539,13 @@ CATALOG_TEMPLATE = '''
393
  </div>
394
  <div class="product-actions">
395
  <button class="product-button" onclick="openQuantityModal({{ loop.index0 }})">
396
- <i class="fas fa-shopping-bag"></i> В корзину
397
  </button>
398
  </div>
399
  </div>
400
  {% endfor %}
401
  {% if not products %}
402
- <p class="no-results-message">Товары пока не добавлены.</p>
403
  {% endif %}
404
  </main>
405
  </div>
@@ -407,38 +553,38 @@ CATALOG_TEMPLATE = '''
407
  <div id="productModal" class="modal">
408
  <div class="modal-content">
409
  <span class="close" onclick="closeModal('productModal')" aria-label="Закрыть">&times;</span>
410
- <div id="modalContent">Загрузка...</div>
411
  </div>
412
  </div>
413
 
414
  <div id="quantityModal" class="modal">
415
  <div class="modal-content">
416
  <span class="close" onclick="closeModal('quantityModal')" aria-label="Закрыть">&times;</span>
417
- <h2>Укажите детали</h2>
418
- <label for="variantSelect">Вариант:</label>
419
  <select id="variantSelect" class="variant-select"></select>
420
 
421
- <label for="quantityInput">Количество:</label>
422
  <input type="number" id="quantityInput" class="quantity-input" min="1" value="1">
423
 
424
- <button class="product-button" onclick="confirmAddToCart()"><i class="fas fa-check"></i> Добавить в корзину</button>
425
  </div>
426
  </div>
427
 
428
  <div id="cartModal" class="modal">
429
  <div class="modal-content">
430
  <span class="close" onclick="closeModal('cartModal')" aria-label="Закрыть">&times;</span>
431
- <h2><i class="fas fa-shopping-cart"></i> Ваша корзина</h2>
432
- <div id="cartContent"><p style="text-align: center; padding: 20px 0;">Ваша корзина пуста.</p></div>
433
  <div class="cart-summary">
434
- <strong>Итого: <span id="cartTotal">0.00</span> {{ currency_code }}</strong>
435
  </div>
436
  <div class="cart-actions">
437
  <button class="product-button clear-cart" onclick="clearCart()">
438
- <i class="fas fa-trash"></i> Очистить
439
  </button>
440
  <button class="product-button formulate-order-button" onclick="formulateOrder()">
441
- <i class="fas fa-file-alt"></i> Оформить заказ
442
  </button>
443
  </div>
444
  </div>
@@ -470,6 +616,8 @@ CATALOG_TEMPLATE = '''
470
  const currencyCode = '{{ currency_code }}';
471
  let selectedProductIndex = null;
472
  let cart = JSON.parse(localStorage.getItem('shaikCart') || '[]');
 
 
473
 
474
  function openModal(index) {
475
  loadProductDetails(index);
@@ -492,8 +640,8 @@ CATALOG_TEMPLATE = '''
492
 
493
  function loadProductDetails(index) {
494
  const modalContent = document.getElementById('modalContent');
495
- modalContent.innerHTML = '<p style="text-align:center; padding: 40px;">Загрузка...</p>';
496
- fetch('/product/' + index)
497
  .then(response => response.text())
498
  .then(data => {
499
  modalContent.innerHTML = data;
@@ -501,7 +649,7 @@ CATALOG_TEMPLATE = '''
501
  })
502
  .catch(error => {
503
  console.error('Ошибка загрузки деталей продукта:', error);
504
- modalContent.innerHTML = `<p style="color: red; text-align:center; padding: 40px;">Не удалось загрузить информацию о товаре.</p>`;
505
  });
506
  }
507
 
@@ -511,7 +659,7 @@ CATALOG_TEMPLATE = '''
511
  pagination: { el: '.swiper-pagination', clickable: true, dynamicBullets: true },
512
  navigation: { nextEl: '.swiper-button-next', prevEl: '.swiper-button-prev' },
513
  zoom: { maxRatio: 3, containerClass: 'swiper-zoom-container' },
514
- autoplay: { delay: 4000, disableOnInteraction: false },
515
  });
516
  }
517
 
@@ -581,7 +729,7 @@ CATALOG_TEMPLATE = '''
581
  localStorage.setItem('shaikCart', JSON.stringify(cart));
582
  closeModal('quantityModal');
583
  updateCartButton();
584
- showNotification(`${product.name} (${selectedVariant.name}) добавлен в корзину!`);
585
  }
586
 
587
  function updateCartButton() {
@@ -603,7 +751,7 @@ CATALOG_TEMPLATE = '''
603
  let total = 0;
604
 
605
  if (cart.length === 0) {
606
- cartContent.innerHTML = '<p style="text-align: center; padding: 20px 0;">Ваша корзина пуста.</p>';
607
  cartTotalElement.textContent = '0.00';
608
  } else {
609
  cartContent.innerHTML = cart.map(item => {
@@ -616,11 +764,11 @@ CATALOG_TEMPLATE = '''
616
  <img src="${photoUrl}" alt="${item.name}">
617
  <div class="cart-item-details">
618
  <strong>${item.name}</strong>
619
- <p class="cart-item-price">Вариант: ${item.variantName}</p>
620
  <p class="cart-item-price">${item.price.toFixed(2)} ${currencyCode} × ${item.quantity}</p>
621
  </div>
622
  <span class="cart-item-total">${itemTotal.toFixed(2)}</span>
623
- <button class="cart-item-remove" onclick="removeFromCart('${item.id}')" title="Удалить товар">&times;</button>
624
  </div>`;
625
  }).join('');
626
  cartTotalElement.textContent = total.toFixed(2);
@@ -637,7 +785,7 @@ CATALOG_TEMPLATE = '''
637
  }
638
 
639
  function clearCart() {
640
- if (confirm("Вы уверены, что хотите очистить корзину?")) {
641
  cart = [];
642
  localStorage.removeItem('shaikCart');
643
  openCartModal();
@@ -647,11 +795,11 @@ CATALOG_TEMPLATE = '''
647
 
648
  function formulateOrder() {
649
  if (cart.length === 0) {
650
- alert("Корзина пуста!");
651
  return;
652
  }
653
  document.querySelector('.formulate-order-button').disabled = true;
654
- showNotification("Формируем заказ...", 5000);
655
 
656
  fetch('/create_order', {
657
  method: 'POST',
@@ -665,7 +813,7 @@ CATALOG_TEMPLATE = '''
665
  cart = [];
666
  updateCartButton();
667
  closeModal('cartModal');
668
- window.location.href = `/order/${data.order_id}`;
669
  } else {
670
  throw new Error(data.error || 'Не получен ID заказа от сервера.');
671
  }
@@ -680,6 +828,8 @@ CATALOG_TEMPLATE = '''
680
  function filterProducts() {
681
  const searchTerm = document.getElementById('search-input').value.toLowerCase().trim();
682
  const activeCategory = document.querySelector('.category-filter.active').dataset.category;
 
 
683
  const grid = document.getElementById('products-grid');
684
  let visibleProducts = 0;
685
 
@@ -690,11 +840,13 @@ CATALOG_TEMPLATE = '''
690
  const name = productElement.dataset.name;
691
  const description = productElement.dataset.description;
692
  const category = productElement.dataset.category;
 
693
 
694
  const matchesSearch = !searchTerm || name.includes(searchTerm) || description.includes(searchTerm);
695
  const matchesCategory = activeCategory === 'all' || category === activeCategory;
 
696
 
697
- if (matchesSearch && matchesCategory) {
698
  productElement.style.display = 'flex';
699
  visibleProducts++;
700
  } else {
@@ -705,7 +857,7 @@ CATALOG_TEMPLATE = '''
705
  if (visibleProducts === 0) {
706
  const msg = document.createElement('p');
707
  msg.className = 'no-results-message';
708
- msg.textContent = products.length > 0 ? 'По вашему запросу товары не найдены.' : 'Товары пока не добавлены.';
709
  grid.appendChild(msg);
710
  }
711
  }
@@ -719,6 +871,13 @@ CATALOG_TEMPLATE = '''
719
  filterProducts();
720
  });
721
  });
 
 
 
 
 
 
 
722
  }
723
 
724
  function showNotification(message, duration = 3000) {
@@ -792,12 +951,13 @@ PRODUCT_DETAIL_TEMPLATE = '''
792
  </div>
793
 
794
  <div style="font-size: 1rem; line-height: 1.7; padding: 0 10px;">
795
- <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
 
796
  {% if product.get('variants') and product.variants|length > 0 %}
797
  <p style="font-size: 1.4rem; font-weight: bold; color: var(--primary-color); margin: 15px 0;">
798
- Цена: от {{ "%.2f"|format(product.variants|map(attribute='price')|min) }} {{ currency_code }}
799
  </p>
800
- <p><strong>Доступные варианты:</strong></p>
801
  <ul style="list-style: none; padding-left: 0;">
802
  {% for variant in product.variants %}
803
  <li style="padding: 5px 0; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between;">
@@ -807,11 +967,11 @@ PRODUCT_DETAIL_TEMPLATE = '''
807
  {% endfor %}
808
  </ul>
809
  {% endif %}
810
- <p style="margin-top: 20px;"><strong>Описание:</strong><br> {{ product.get('description', 'Описание отсутствует.')|replace('\\n', '<br>')|safe }}</p>
811
  </div>
812
  <div style="padding: 20px 10px 10px; text-align: center;">
813
  <button class="product-button" onclick="closeModal('productModal'); openQuantityModal({{ products_index }})" style="margin-top: 15px; padding: 12px 25px; font-size: 1rem;">
814
- <i class="fas fa-shopping-bag"></i> Добавить в корзину
815
  </button>
816
  </div>
817
  </div>
@@ -819,11 +979,11 @@ PRODUCT_DETAIL_TEMPLATE = '''
819
 
820
  ORDER_TEMPLATE = '''
821
  <!DOCTYPE html>
822
- <html lang="ru">
823
  <head>
824
  <meta charset="UTF-8">
825
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
826
- <title>Заказ {{ order.id }} - SHAIK парфюм</title>
827
  <link rel="preconnect" href="https://fonts.googleapis.com">
828
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
829
  <link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@400;600;700&family=Georgia&display=swap" rel="stylesheet">
@@ -867,17 +1027,17 @@ ORDER_TEMPLATE = '''
867
  <body>
868
  <div class="container">
869
  {% if order %}
870
- <h1><i class="fas fa-receipt"></i> Заказ {{ order.id }}</h1>
871
- <p class="order-meta">Дата создания: {{ order.created_at }}</p>
872
 
873
- <h2><i class="fas fa-shopping-bag"></i> Товары в заказе</h2>
874
  <div id="orderItems">
875
  {% for item in order.cart %}
876
  <div class="order-item">
877
  <img src="{{ item.photo_url }}" alt="{{ item.name }}">
878
  <div class="item-details">
879
  <strong>{{ item.name }}</strong>
880
- <span>Вариант: {{ item.variantName }}</span>
881
  <span>{{ "%.2f"|format(item.price) }} {{ currency_code }} × {{ item.quantity }}</span>
882
  </div>
883
  <div class="item-total">
@@ -888,20 +1048,20 @@ ORDER_TEMPLATE = '''
888
  </div>
889
 
890
  <div class="order-summary">
891
- <p>Общая сумма товаров: <strong>{{ "%.2f"|format(order.total_price) }} {{ currency_code }}</strong></p>
892
  </div>
893
 
894
  <div class="customer-info">
895
- <h2><i class="fas fa-info-circle"></i> Статус и подтверждение</h2>
896
- <p>Текущий статус: <strong>{{ status_map_ru.get(order.status, order.status) }}</strong></p>
897
- <p>Пожалуйста, свяжитесь с нами по WhatsApp для подтверждения заказа и уточнения деталей доставки и оплаты.</p>
898
  </div>
899
 
900
  <div class="actions">
901
- <button class="button" onclick="sendOrderViaWhatsApp()"><i class="fab fa-whatsapp"></i> Связаться с нами</button>
902
  </div>
903
 
904
- <a href="{{ url_for('catalog') }}" class="catalog-link">&larr; Вернуться в каталог</a>
905
 
906
  <script>
907
  function sendOrderViaWhatsApp() {
@@ -909,10 +1069,10 @@ ORDER_TEMPLATE = '''
909
  const orderUrl = `{{ request.url }}`;
910
  const whatsappNumber = "77762021169";
911
 
912
- let message = `Здравствуйте! Хочу подтвердить свой заказ на SHAIK парфюм:%0A%0A`;
913
- message += `*Номер заказа:* ${orderId}%0A`;
914
- message += `*Ссылка на заказ:* ${encodeURIComponent(orderUrl)}%0A%0A`;
915
- message += `Пожалуйста, свяжитесь со мной для уточнения деталей.`;
916
 
917
  const whatsappUrl = `https://api.whatsapp.com/send?phone=${whatsappNumber}&text=${message}`;
918
  window.open(whatsappUrl, '_blank');
@@ -920,9 +1080,9 @@ ORDER_TEMPLATE = '''
920
  </script>
921
 
922
  {% else %}
923
- <h1 style="color: #ff453a;"><i class="fas fa-exclamation-triangle"></i> Ошибка</h1>
924
- <p class="not-found">Заказ с таким ID не найден.</p>
925
- <a href="{{ url_for('catalog') }}" class="catalog-link">&larr; Вернуться в каталог</a>
926
  {% endif %}
927
  </div>
928
  </body>
@@ -1009,8 +1169,8 @@ ADMIN_TEMPLATE = '''
1009
  .order-status-form { display: flex; gap: 10px; align-items: center; margin-top: 15px; padding-top: 15px; border-top: 1px dashed var(--border-color); flex-wrap: wrap; }
1010
  .order-status-form select { max-width: 180px; margin-top: 0; flex-grow: 1; }
1011
 
1012
- .flex-container { display: flex; flex-wrap: wrap; gap: 20px; }
1013
- .flex-item { flex: 1; min-width: 300px; }
1014
  </style>
1015
  </head>
1016
  <body>
@@ -1020,7 +1180,7 @@ ADMIN_TEMPLATE = '''
1020
  <img src="https://huggingface.co/spaces/Shaik-parfume/app/resolve/main/icon.png" alt="SHAIK Logo" style="height: 45px; width: 45px; border-radius: 50%;">
1021
  <h1><i class="fas fa-tools"></i> Админ-панель</h1>
1022
  </div>
1023
- <a href="{{ url_for('catalog') }}" class="button"><i class="fas fa-store"></i> Перейти в каталог</a>
1024
  </div>
1025
 
1026
 
@@ -1034,7 +1194,6 @@ ADMIN_TEMPLATE = '''
1034
 
1035
  <div class="section">
1036
  <h2><i class="fas fa-history"></i> История заказов</h2>
1037
-
1038
  {% if orders %}
1039
  {% for order in orders | sort(attribute='created_at', reverse=true) %}
1040
  {% set current_status = order.get('status', 'new') %}
@@ -1061,7 +1220,7 @@ ADMIN_TEMPLATE = '''
1061
  {% endfor %}
1062
  </select>
1063
  <button type="submit" class="button" style="margin-top:0;"><i class="fas fa-check"></i> Сохранить</button>
1064
- <a href="{{ url_for('view_order', order_id=order.id) }}" target="_blank" class="button" style="background-color: #444; color: white; margin-top:0;"><i class="fas fa-eye"></i> Просмотр</a>
1065
  </form>
1066
  </div>
1067
  </div>
@@ -1088,7 +1247,6 @@ ADMIN_TEMPLATE = '''
1088
  </form>
1089
  </div>
1090
  </details>
1091
-
1092
  <h3>Существующие категории:</h3>
1093
  {% if categories %}
1094
  <div class="item-list">
@@ -1110,15 +1268,36 @@ ADMIN_TEMPLATE = '''
1110
  </div>
1111
 
1112
  <div class="flex-item">
1113
- <div class="section">
1114
- <h2><i class="fas fa-info-circle"></i> Инфо о магазине</h2>
1115
- <p><strong>Адреса в г. Алматы:</strong></p>
1116
- <ul style="padding-left: 20px;">
1117
- {% for address in store_addresses %}
1118
- <li>{{ address }}</li>
1119
- {% endfor %}
1120
- </ul>
1121
- <p style="margin-top: 10px;"><strong>Валюта:</strong> {{ currency_name }} ({{ currency_code }})</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1122
  </div>
1123
  </div>
1124
  </div>
@@ -1134,6 +1313,7 @@ ADMIN_TEMPLATE = '''
1134
  <input type="text" id="add_name" name="name" required>
1135
  <label for="add_description">Описание:</label>
1136
  <textarea id="add_description" name="description" rows="4"></textarea>
 
1137
  <label for="add_category">Категория:</label>
1138
  <select id="add_category" name="category">
1139
  <option value="Без категории">Без категории</option>
@@ -1141,6 +1321,15 @@ ADMIN_TEMPLATE = '''
1141
  <option value="{{ category }}">{{ category }}</option>
1142
  {% endfor %}
1143
  </select>
 
 
 
 
 
 
 
 
 
1144
  <label for="add_photos">Фотографии (до 10 шт.):</label>
1145
  <input type="file" id="add_photos" name="photos" accept="image/*" multiple>
1146
 
@@ -1185,7 +1374,7 @@ ADMIN_TEMPLATE = '''
1185
  {% if product.get('is_top', False) %}<span class="status-indicator top-product"><i class="fas fa-star fa-xs"></i> Топ</span>{% endif %}
1186
  </h3>
1187
  <p style="font-size: 0.9rem; color: var(--text-color-muted);"><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
1188
- <p style="font-size: 0.9rem; color: var(--text-color-muted);"><strong>Варианты:</strong> {{ product.get('variants', [])|length }} шт.</p>
1189
  </div>
1190
  </div>
1191
 
@@ -1207,6 +1396,7 @@ ADMIN_TEMPLATE = '''
1207
  <input type="text" name="name" value="{{ product['name'] }}" required>
1208
  <label>Описание:</label>
1209
  <textarea name="description" rows="4">{{ product.get('description', '') }}</textarea>
 
1210
  <label>Категория:</label>
1211
  <select name="category">
1212
  <option value="Без категории" {% if product.get('category', 'Без категории') == 'Без категории' %}selected{% endif %}>Без категории</option>
@@ -1214,6 +1404,14 @@ ADMIN_TEMPLATE = '''
1214
  <option value="{{ category }}" {% if product.get('category') == category %}selected{% endif %}>{{ category }}</option>
1215
  {% endfor %}
1216
  </select>
 
 
 
 
 
 
 
 
1217
 
1218
  <label>Текущие фотографии (отметьте для удаления):</label>
1219
  <div class="photo-preview-edit">
@@ -1259,7 +1457,16 @@ ADMIN_TEMPLATE = '''
1259
  <p>Товаров пока нет.</p>
1260
  {% endif %}
1261
  </div>
1262
-
 
 
 
 
 
 
 
 
 
1263
  </div>
1264
 
1265
  <script>
@@ -1295,12 +1502,25 @@ ADMIN_TEMPLATE = '''
1295
  '''
1296
 
1297
 
 
 
 
 
 
 
 
 
1298
 
1299
  @app.route('/')
1300
- def catalog():
 
 
 
 
1301
  data = load_data()
1302
  all_products = data.get('products', [])
1303
  categories = sorted(data.get('categories', []))
 
1304
 
1305
  products_in_stock = [p for p in all_products if p.get('variants')]
1306
 
@@ -1313,13 +1533,16 @@ def catalog():
1313
  CATALOG_TEMPLATE,
1314
  products=products_sorted,
1315
  categories=categories,
 
1316
  repo_id=REPO_ID,
1317
  store_addresses=STORE_ADDRESSES,
1318
- currency_code=CURRENCY_CODE
 
 
1319
  )
1320
 
1321
- @app.route('/product/<int:index>')
1322
- def product_detail(index):
1323
  data = load_data()
1324
  all_products = data.get('products', [])
1325
  products_in_stock = [p for p in all_products if p.get('variants')]
@@ -1339,7 +1562,9 @@ def product_detail(index):
1339
  product=product,
1340
  products_index=index,
1341
  repo_id=REPO_ID,
1342
- currency_code=CURRENCY_CODE
 
 
1343
  )
1344
 
1345
  @app.route('/create_order', methods=['POST'])
@@ -1402,11 +1627,12 @@ def create_order():
1402
  logging.error(f"Failed to save order {order_id}: {e}")
1403
  return jsonify({"error": "Ошибка сервера."}), 500
1404
 
1405
- @app.route('/order/<order_id>')
1406
- def view_order(order_id):
1407
  data = load_data()
1408
  order = data.get('orders', {}).get(order_id)
1409
- return render_template_string(ORDER_TEMPLATE, order=order, status_map_ru=STATUS_MAP_RU, currency_code=CURRENCY_CODE)
 
1410
 
1411
  @app.route('/admin', methods=['GET', 'POST'])
1412
  def admin():
@@ -1427,7 +1653,7 @@ def admin():
1427
  if action == 'update_order_status':
1428
  order_id = request.form.get('order_id')
1429
  new_status = request.form.get('new_status')
1430
- if order_id in data['orders'] and new_status in STATUS_MAP_RU:
1431
  data['orders'][order_id]['status'] = new_status
1432
  save_data(data)
1433
  flash(f"Статус заказа №{order_id} изменен.", 'success')
@@ -1455,6 +1681,27 @@ def admin():
1455
  else:
1456
  flash("Категория не найдена.", 'error')
1457
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1458
  elif action == 'add_product' or action == 'edit_product':
1459
  name = request.form.get('name', '').strip()
1460
  if not name:
@@ -1482,6 +1729,7 @@ def admin():
1482
  'name': name,
1483
  'description': request.form.get('description', '').strip(),
1484
  'category': request.form.get('category'),
 
1485
  'is_top': 'is_top' in request.form,
1486
  'variants': variants
1487
  }
@@ -1521,7 +1769,7 @@ def admin():
1521
  data['products'].append(product_data)
1522
  flash(f"Товар '{name}' добавлен.", 'success')
1523
 
1524
- else: # edit_product
1525
  product_id = request.form.get('product_id')
1526
  product_index = next((i for i, p in enumerate(data['products']) if p.get('id') == product_id), -1)
1527
  if product_index != -1:
@@ -1581,8 +1829,9 @@ def admin():
1581
  ADMIN_TEMPLATE,
1582
  products=sorted(current_data.get('products', []), key=lambda p: p.get('name', '').lower()),
1583
  categories=sorted(current_data.get('categories', [])),
 
1584
  orders=list(current_data.get('orders', {}).values()),
1585
- status_map_ru=STATUS_MAP_RU,
1586
  repo_id=REPO_ID,
1587
  store_addresses=STORE_ADDRESSES,
1588
  currency_code=CURRENCY_CODE,
@@ -1596,4 +1845,4 @@ if __name__ == '__main__':
1596
  if HF_TOKEN_WRITE:
1597
  threading.Thread(target=periodic_backup, daemon=True).start()
1598
  port = int(os.environ.get('PORT', 7860))
1599
- app.run(debug=False, host='0.0.0.0', port=port)
 
1
+
2
+ from flask import Flask, render_template_string, request, redirect, url_for, send_file, flash, jsonify, g
3
  import json
4
  import os
5
  import logging
 
38
  DOWNLOAD_RETRIES = 3
39
  DOWNLOAD_DELAY = 5
40
 
41
+ STATUS_MAPS = {
42
+ 'ru': {
43
+ "new": "Новый",
44
+ "accepted": "Принят",
45
+ "prepared": "Собран",
46
+ "shipped": "Отправлен"
47
+ },
48
+ 'kz': {
49
+ "new": "Жаңа",
50
+ "accepted": "Қабылданды",
51
+ "prepared": "Жиналды",
52
+ "shipped": "Жөнелтілді"
53
+ }
54
+ }
55
+
56
+ translations = {
57
+ 'ru': {
58
+ 'page_title': "SHAIK парфюм оптом и в розницу - Каталог",
59
+ 'header_title': "SHAIK Parfum",
60
+ 'our_addresses': "Наши адреса в г. Алматы",
61
+ 'search_placeholder': "Поиск по названию или описанию...",
62
+ 'all_filter': "Все",
63
+ 'top_product': "Топ",
64
+ 'add_to_cart_button': "В корзину",
65
+ 'from_price': "от",
66
+ 'no_products_yet': "Товары пока не добавлены.",
67
+ 'loading': "Загрузка...",
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': "Ваша корзина пуста.",
75
+ 'total': "Итого:",
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': "Корзина пуста!",
82
+ 'formulating_order': "Формируем заказ...",
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': "Заказ №",
93
+ 'order_created_at': "Дата создания:",
94
+ 'order_items': "Товары в заказе",
95
+ 'order_total_price': "Общая сумма товаров:",
96
+ 'order_status_and_confirmation': "Статус и подтверждение",
97
+ 'order_current_status': "Текущий статус:",
98
+ 'order_whatsapp_prompt': "Пожалуйста, свяжитесь с нами по WhatsApp для подтверждения заказа и уточнения деталей доставки и оплаты.",
99
+ 'order_contact_us_button': "Связаться с нами",
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': "Барлығы",
114
+ 'top_product': "Топ",
115
+ 'add_to_cart_button': "Себетке салу",
116
+ 'from_price': "-ден бастап",
117
+ 'no_products_yet': "Тауарлар әлі қосылмаған.",
118
+ 'loading': "Жүктелуде...",
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': "Сіздің себетіңіз бос.",
126
+ 'total': "Жиыны:",
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': "Себет бос!",
133
+ 'formulating_order': "Тапсырыс қалыптастырылуда...",
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': "Тапсырыс №",
144
+ 'order_created_at': "Құрылған күні:",
145
+ 'order_items': "Тапсырыстағы тауарлар",
146
+ 'order_total_price': "Тауарлардың жалпы сомасы:",
147
+ 'order_status_and_confirmation': "Мәртебесі және растау",
148
+ 'order_current_status': "Ағымдағы мәртебе:",
149
+ 'order_whatsapp_prompt': "Тапсырысты растау және жеткізу мен төлем туралы мәліметтерді нақтылау үшін WhatsApp арқылы бізбен хабарласыңыз.",
150
+ 'order_contact_us_button': "Бізбен байланысу",
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': "Мәліметтерді нақтылау үшін менімен хабарласуыңызды сұраймын.",
158
+ }
159
  }
160
 
161
+
162
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
163
 
164
 
 
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
 
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
  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:
 
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:
 
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:
 
337
 
338
  CATALOG_TEMPLATE = '''
339
  <!DOCTYPE html>
340
+ <html lang="{{ lang_code }}">
341
  <head>
342
  <meta charset="UTF-8">
343
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
344
+ <title>{{ _['page_title'] }}</title>
345
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
346
  <link rel="preconnect" href="https://fonts.googleapis.com">
347
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
 
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; }
378
+ .lang-switcher a { color: var(--text-color-muted); text-decoration: none; font-size: 0.9rem; padding: 5px 10px; border-radius: 50px; transition: all 0.3s; }
379
+ .lang-switcher a.active { background-color: var(--primary-color); color: white; font-weight: bold; }
380
+
381
  .store-addresses { padding: 20px; text-align: center; background-color: var(--surface-color); border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.5); font-size: 0.95rem; color: var(--text-color-muted); margin: 20px; }
382
  .store-addresses h3 { font-family: 'Cormorant Garamond', serif; color: var(--primary-color); font-size: 1.3rem; margin-bottom: 10px; font-weight: 600; display: flex; align-items: center; justify-content: center; gap: 8px; }
383
  .store-addresses p { margin: 5px 0; }
 
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; } }
 
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">
470
+ <a href="/ru{{ request.path.replace('/kz', '') }}" class="{{ 'active' if lang_code == 'ru' else '' }}">RU</a>
471
+ <a href="/kz{{ request.path.replace('/ru', '') }}" class="{{ 'active' if lang_code == 'kz' else '' }}">KZ</a>
472
  </div>
473
  </header>
474
 
475
  <div class="store-addresses">
476
+ <h3><i class="fas fa-map-marker-alt"></i> {{ _['our_addresses'] }}</h3>
477
  {% for address in store_addresses %}
478
  <p>{{ address }}</p>
479
  {% endfor %}
 
481
 
482
 
483
  <div class="search-container">
484
+ <input type="text" id="search-input" placeholder="{{ _['search_placeholder'] }}">
485
  </div>
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
+
511
  <main class="products-grid" id="products-grid">
512
  {% for product in products %}
513
  <div class="product"
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 %}
521
  <div class="product-image" onclick="openModal({{ loop.index0 }})" style="cursor: pointer;">
522
  {% if product.get('photos') and product['photos']|length > 0 %}
 
531
  <h2>{{ product['name'] }}</h2>
532
  <div class="product-price">
533
  {% if product.get('variants') and product.variants|length > 0 %}
534
+ <span class="from-text">{{ _['from_price'] }}</span> {{ "%.2f"|format(product.variants|map(attribute='price')|min) }} {{ currency_code }}
535
  {% else %}
536
  -
537
  {% endif %}
 
539
  </div>
540
  <div class="product-actions">
541
  <button class="product-button" onclick="openQuantityModal({{ loop.index0 }})">
542
+ <i class="fas fa-shopping-bag"></i> {{ _['add_to_cart_button'] }}
543
  </button>
544
  </div>
545
  </div>
546
  {% endfor %}
547
  {% if not products %}
548
+ <p class="no-results-message">{{ _['no_products_yet'] }}</p>
549
  {% endif %}
550
  </main>
551
  </div>
 
553
  <div id="productModal" class="modal">
554
  <div class="modal-content">
555
  <span class="close" onclick="closeModal('productModal')" aria-label="Закрыть">&times;</span>
556
+ <div id="modalContent">{{ _['loading'] }}</div>
557
  </div>
558
  </div>
559
 
560
  <div id="quantityModal" class="modal">
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>
572
  </div>
573
 
574
  <div id="cartModal" class="modal">
575
  <div class="modal-content">
576
  <span class="close" onclick="closeModal('cartModal')" aria-label="Закрыть">&times;</span>
577
+ <h2><i class="fas fa-shopping-cart"></i> {{ _['your_cart'] }}</h2>
578
+ <div id="cartContent"><p style="text-align: center; padding: 20px 0;">{{ _['cart_is_empty'] }}</p></div>
579
  <div class="cart-summary">
580
+ <strong>{{ _['total'] }} <span id="cartTotal">0.00</span> {{ currency_code }}</strong>
581
  </div>
582
  <div class="cart-actions">
583
  <button class="product-button clear-cart" onclick="clearCart()">
584
+ <i class="fas fa-trash"></i> {{ _['clear_cart_button'] }}
585
  </button>
586
  <button class="product-button formulate-order-button" onclick="formulateOrder()">
587
+ <i class="fas fa-file-alt"></i> {{ _['formulate_order_button'] }}
588
  </button>
589
  </div>
590
  </div>
 
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
 
622
  function openModal(index) {
623
  loadProductDetails(index);
 
640
 
641
  function loadProductDetails(index) {
642
  const modalContent = document.getElementById('modalContent');
643
+ modalContent.innerHTML = `<p style="text-align:center; padding: 40px;">${translations['loading']}</p>`;
644
+ fetch(`/${langCode}/product/${index}`)
645
  .then(response => response.text())
646
  .then(data => {
647
  modalContent.innerHTML = data;
 
649
  })
650
  .catch(error => {
651
  console.error('Ошибка загрузки деталей продукта:', error);
652
+ modalContent.innerHTML = `<p style="color: red; text-align:center; padding: 40px;">${translations['product_load_error']}</p>`;
653
  });
654
  }
655
 
 
659
  pagination: { el: '.swiper-pagination', clickable: true, dynamicBullets: true },
660
  navigation: { nextEl: '.swiper-button-next', prevEl: '.swiper-button-prev' },
661
  zoom: { maxRatio: 3, containerClass: 'swiper-zoom-container' },
662
+ autoplay: { delay: 4000, disableOnInteraction: true },
663
  });
664
  }
665
 
 
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() {
 
751
  let total = 0;
752
 
753
  if (cart.length === 0) {
754
+ cartContent.innerHTML = `<p style="text-align: center; padding: 20px 0;">${translations['cart_is_empty']}</p>`;
755
  cartTotalElement.textContent = '0.00';
756
  } else {
757
  cartContent.innerHTML = cart.map(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>
772
  </div>`;
773
  }).join('');
774
  cartTotalElement.textContent = total.toFixed(2);
 
785
  }
786
 
787
  function clearCart() {
788
+ if (confirm(translations['clear_cart_confirm'])) {
789
  cart = [];
790
  localStorage.removeItem('shaikCart');
791
  openCartModal();
 
795
 
796
  function formulateOrder() {
797
  if (cart.length === 0) {
798
+ alert(translations['cart_is_empty_alert']);
799
  return;
800
  }
801
  document.querySelector('.formulate-order-button').disabled = true;
802
+ showNotification(translations['formulating_order'], 5000);
803
 
804
  fetch('/create_order', {
805
  method: 'POST',
 
813
  cart = [];
814
  updateCartButton();
815
  closeModal('cartModal');
816
+ window.location.href = `/${langCode}/order/${data.order_id}`;
817
  } else {
818
  throw new Error(data.error || 'Не получен ID заказа от сервера.');
819
  }
 
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
  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 {
 
857
  if (visibleProducts === 0) {
858
  const msg = document.createElement('p');
859
  msg.className = 'no-results-message';
860
+ msg.textContent = products.length > 0 ? translations['no_products_found'] : translations['no_products_yet'];
861
  grid.appendChild(msg);
862
  }
863
  }
 
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
+ });
880
+ });
881
  }
882
 
883
  function showNotification(message, duration = 3000) {
 
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 }}
959
  </p>
960
+ <p><strong>{{ _['available_variants'] }}</strong></p>
961
  <ul style="list-style: none; padding-left: 0;">
962
  {% for variant in product.variants %}
963
  <li style="padding: 5px 0; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between;">
 
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;">
973
  <button class="product-button" onclick="closeModal('productModal'); openQuantityModal({{ products_index }})" style="margin-top: 15px; padding: 12px 25px; font-size: 1rem;">
974
+ <i class="fas fa-shopping-bag"></i> {{ _['add_to_cart_button'] }}
975
  </button>
976
  </div>
977
  </div>
 
979
 
980
  ORDER_TEMPLATE = '''
981
  <!DOCTYPE html>
982
+ <html lang="{{ lang_code }}">
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">
 
1027
  <body>
1028
  <div class="container">
1029
  {% if order %}
1030
+ <h1><i class="fas fa-receipt"></i> {{ _['order_page_title'] }}{{ order.id }}</h1>
1031
+ <p class="order-meta">{{ _['order_created_at'] }} {{ order.created_at }}</p>
1032
 
1033
+ <h2><i class="fas fa-shopping-bag"></i> {{ _['order_items'] }}</h2>
1034
  <div id="orderItems">
1035
  {% for item in order.cart %}
1036
  <div class="order-item">
1037
  <img src="{{ item.photo_url }}" alt="{{ item.name }}">
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">
 
1048
  </div>
1049
 
1050
  <div class="order-summary">
1051
+ <p>{{ _['order_total_price'] }}: <strong>{{ "%.2f"|format(order.total_price) }} {{ currency_code }}</strong></p>
1052
  </div>
1053
 
1054
  <div class="customer-info">
1055
+ <h2><i class="fas fa-info-circle"></i> {{ _['order_status_and_confirmation'] }}</h2>
1056
+ <p>{{ _['order_current_status'] }}: <strong>{{ status_map.get(order.status, order.status) }}</strong></p>
1057
+ <p>{{ _['order_whatsapp_prompt'] }}</p>
1058
  </div>
1059
 
1060
  <div class="actions">
1061
+ <button class="button" onclick="sendOrderViaWhatsApp()"><i class="fab fa-whatsapp"></i> {{ _['order_contact_us_button'] }}</button>
1062
  </div>
1063
 
1064
+ <a href="{{ url_for('catalog', lang_code=lang_code) }}" class="catalog-link">&larr; {{ _['return_to_catalog'] }}</a>
1065
 
1066
  <script>
1067
  function sendOrderViaWhatsApp() {
 
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`;
1074
+ message += `*{{ _['whatsapp_confirm_message_3'] }}* ${encodeURIComponent(orderUrl)}%0A%0A`;
1075
+ message += `{{ _['whatsapp_confirm_message_4'] }}`;
1076
 
1077
  const whatsappUrl = `https://api.whatsapp.com/send?phone=${whatsappNumber}&text=${message}`;
1078
  window.open(whatsappUrl, '_blank');
 
1080
  </script>
1081
 
1082
  {% else %}
1083
+ <h1 style="color: #ff453a;"><i class="fas fa-exclamation-triangle"></i> {{ _['order_not_found_error'] }}</h1>
1084
+ <p class="not-found">{{ _['order_not_found_message'] }}</p>
1085
+ <a href="{{ url_for('catalog', lang_code=lang_code) }}" class="catalog-link">&larr; {{ _['return_to_catalog'] }}</a>
1086
  {% endif %}
1087
  </div>
1088
  </body>
 
1169
  .order-status-form { display: flex; gap: 10px; align-items: center; margin-top: 15px; padding-top: 15px; border-top: 1px dashed var(--border-color); flex-wrap: wrap; }
1170
  .order-status-form select { max-width: 180px; margin-top: 0; flex-grow: 1; }
1171
 
1172
+ .flex-container { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; }
1173
+ .flex-item { min-width: 0; }
1174
  </style>
1175
  </head>
1176
  <body>
 
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>
1185
 
1186
 
 
1194
 
1195
  <div class="section">
1196
  <h2><i class="fas fa-history"></i> История заказов</h2>
 
1197
  {% if orders %}
1198
  {% for order in orders | sort(attribute='created_at', reverse=true) %}
1199
  {% set current_status = order.get('status', 'new') %}
 
1220
  {% endfor %}
1221
  </select>
1222
  <button type="submit" class="button" style="margin-top:0;"><i class="fas fa-check"></i> Сохранить</button>
1223
+ <a href="{{ url_for('view_order', lang_code='ru', order_id=order.id) }}" target="_blank" class="button" style="background-color: #444; color: white; margin-top:0;"><i class="fas fa-eye"></i> Просмотр</a>
1224
  </form>
1225
  </div>
1226
  </div>
 
1247
  </form>
1248
  </div>
1249
  </details>
 
1250
  <h3>Существующие категории:</h3>
1251
  {% if categories %}
1252
  <div class="item-list">
 
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>
 
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
+
1317
  <label for="add_category">Категория:</label>
1318
  <select id="add_category" name="category">
1319
  <option value="Без категории">Без категории</option>
 
1321
  <option value="{{ category }}">{{ category }}</option>
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
 
 
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
 
 
1396
  <input type="text" name="name" value="{{ product['name'] }}" required>
1397
  <label>Описание:</label>
1398
  <textarea name="description" rows="4">{{ product.get('description', '') }}</textarea>
1399
+
1400
  <label>Категория:</label>
1401
  <select name="category">
1402
  <option value="Без категории" {% if product.get('category', 'Без категории') == 'Без категории' %}selected{% endif %}>Без категории</option>
 
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">
 
1457
  <p>Товаров пока нет.</p>
1458
  {% endif %}
1459
  </div>
1460
+ <div class="section">
1461
+ <h2><i class="fas fa-info-circle"></i> Инфо о магазине</h2>
1462
+ <p><strong>Адреса в г. Алматы:</strong></p>
1463
+ <ul style="padding-left: 20px;">
1464
+ {% for address in store_addresses %}
1465
+ <li>{{ address }}</li>
1466
+ {% endfor %}
1467
+ </ul>
1468
+ <p style="margin-top: 10px;"><strong>Валюта:</strong> {{ currency_name }} ({{ currency_code }})</p>
1469
+ </div>
1470
  </div>
1471
 
1472
  <script>
 
1502
  '''
1503
 
1504
 
1505
+ @app.before_request
1506
+ def before_request():
1507
+ path_parts = request.path.split('/')
1508
+ if len(path_parts) > 1 and path_parts[1] in translations:
1509
+ g.lang_code = path_parts[1]
1510
+ else:
1511
+ g.lang_code = 'ru'
1512
+ g.translations = translations.get(g.lang_code, translations['ru'])
1513
 
1514
  @app.route('/')
1515
+ def index():
1516
+ return redirect(url_for('catalog', lang_code='ru'))
1517
+
1518
+ @app.route('/<lang_code>/')
1519
+ 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
  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>')
1545
+ def product_detail(lang_code, index):
1546
  data = load_data()
1547
  all_products = data.get('products', [])
1548
  products_in_stock = [p for p in all_products if p.get('variants')]
 
1562
  product=product,
1563
  products_index=index,
1564
  repo_id=REPO_ID,
1565
+ currency_code=CURRENCY_CODE,
1566
+ lang_code=g.lang_code,
1567
+ _=g.translations
1568
  )
1569
 
1570
  @app.route('/create_order', methods=['POST'])
 
1627
  logging.error(f"Failed to save order {order_id}: {e}")
1628
  return jsonify({"error": "Ошибка сервера."}), 500
1629
 
1630
+ @app.route('/<lang_code>/order/<order_id>')
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():
 
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']:
1657
  data['orders'][order_id]['status'] = new_status
1658
  save_data(data)
1659
  flash(f"Статус заказа №{order_id} изменен.", 'success')
 
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:
 
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
  }
 
1769
  data['products'].append(product_data)
1770
  flash(f"Товар '{name}' добавлен.", 'success')
1771
 
1772
+ else:
1773
  product_id = request.form.get('product_id')
1774
  product_index = next((i for i, p in enumerate(data['products']) if p.get('id') == product_id), -1)
1775
  if product_index != -1:
 
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,
1837
  currency_code=CURRENCY_CODE,
 
1845
  if HF_TOKEN_WRITE:
1846
  threading.Thread(target=periodic_backup, daemon=True).start()
1847
  port = int(os.environ.get('PORT', 7860))
1848
+ app.run(debug=False, host='0.0.0.0', port=port)