Kgshop commited on
Commit
d472dcd
·
verified ·
1 Parent(s): 8be71fc

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +132 -111
app.py CHANGED
@@ -30,20 +30,22 @@ def load_data():
30
  data = json.load(file)
31
  logging.info("Данные успешно загружены из JSON")
32
  if not isinstance(data, dict) or 'products' not in data or 'categories' not in data:
33
- return {'products': [], 'categories': [] if not isinstance(data, list) else data}
 
 
34
  return data
35
  except FileNotFoundError:
36
  logging.warning("Локальный файл базы данных не найден после скачивания.")
37
- return {'products': [], 'categories': []}
38
  except json.JSONDecodeError:
39
  logging.error("Ошибка: Невозможно декодировать JSON файл.")
40
- return {'products': [], 'categories': []}
41
  except RepositoryNotFoundError:
42
  logging.error("Репозиторий не найден. Создание локальной базы данных.")
43
- return {'products': [], 'categories': []}
44
  except Exception as e:
45
  logging.error(f"Произошла ошибка при загрузке данных: {e}")
46
- return {'products': [], 'categories': []}
47
 
48
  def save_data(data):
49
  try:
@@ -94,18 +96,19 @@ def periodic_backup():
94
  time.sleep(800)
95
 
96
  @app.route('/')
97
- def catalog():
98
  data = load_data()
99
  products = data['products']
100
  categories = data['categories']
 
101
 
102
- catalog_html = '''
103
  <!DOCTYPE html>
104
  <html lang="ru">
105
  <head>
106
  <meta charset="UTF-8">
107
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
108
- <title>Routine wholesale - Женская одежда</title>
109
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
110
  <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
111
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.css">
@@ -384,7 +387,7 @@ def catalog():
384
  border-radius: 8px;
385
  margin-right: 15px;
386
  }
387
- .quantity-input, .color-select {
388
  width: 100%;
389
  max-width: 150px;
390
  padding: 8px;
@@ -393,6 +396,9 @@ def catalog():
393
  font-size: 1rem;
394
  margin: 5px 0;
395
  }
 
 
 
396
  .clear-cart {
397
  background-color: #ef4444;
398
  }
@@ -413,7 +419,7 @@ def catalog():
413
  <div class="container">
414
  <div class="header">
415
  <img src="''' + LOGO_URL + '''" alt="Logo" class="header-logo">
416
- <h1>Каталог</h1>
417
  <button class="theme-toggle" onclick="toggleTheme()">
418
  <i class="fas fa-moon"></i>
419
  </button>
@@ -425,7 +431,7 @@ def catalog():
425
  {% endfor %}
426
  </div>
427
  <div class="search-container">
428
- <input type="text" id="search-input" placeholder="Поиск товаров...">
429
  </div>
430
  <div class="products-grid" id="products-grid">
431
  {% for product in products %}
@@ -444,7 +450,7 @@ def catalog():
444
  <div class="product-price">{{ product['price'] }} с</div>
445
  <p class="product-description">{{ product['description'][:50] }}{% if product['description']|length > 50 %}...{% endif %}</p>
446
  <button class="product-button" onclick="openModal({{ loop.index0 }})">Подробнее</button>
447
- <button class="product-button add-to-cart" onclick="openQuantityModal({{ loop.index0 }})">В корзину</button>
448
  </div>
449
  {% endfor %}
450
  </div>
@@ -458,13 +464,13 @@ def catalog():
458
  </div>
459
  </div>
460
 
461
- <!-- Quantity and Color Modal -->
462
- <div id="quantityModal" class="modal">
463
  <div class="modal-content">
464
- <span class="close" onclick="closeModal('quantityModal')">×</span>
465
- <h2>Укажит�� количество и цвет</h2>
466
  <input type="number" id="quantityInput" class="quantity-input" min="1" value="1">
467
- <select id="colorSelect" class="color-select"></select>
468
  <button class="product-button" onclick="confirmAddToCart()">Добавить</button>
469
  </div>
470
  </div>
@@ -490,6 +496,7 @@ def catalog():
490
  <script src="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.js"></script>
491
  <script>
492
  const products = {{ products|tojson }};
 
493
  let selectedProductIndex = null;
494
 
495
  function toggleTheme() {
@@ -536,56 +543,47 @@ def catalog():
536
  });
537
  }
538
 
539
- function openQuantityModal(index) {
540
  selectedProductIndex = index;
541
  const product = products[index];
542
- const colorSelect = document.getElementById('colorSelect');
543
- colorSelect.innerHTML = '';
544
- if (product.colors && product.colors.length > 0) {
545
- product.colors.forEach(color => {
546
- const option = document.createElement('option');
547
- option.value = color;
548
- option.text = color;
549
- colorSelect.appendChild(option);
550
- });
551
- } else {
552
- const option = document.createElement('option');
553
- option.value = 'Нет цвета';
554
- option.text = 'Нет цвета';
555
- colorSelect.appendChild(option);
556
- }
557
- document.getElementById('quantityModal').style.display = 'block';
558
  document.getElementById('quantityInput').value = 1;
559
  }
560
 
561
  function confirmAddToCart() {
562
  if (selectedProductIndex === null) return;
563
  const quantity = parseInt(document.getElementById('quantityInput').value) || 1;
564
- const color = document.getElementById('colorSelect').value;
565
  if (quantity <= 0) {
566
  alert("Укажите количество больше 0");
567
  return;
568
  }
569
  let cart = JSON.parse(localStorage.getItem('cart') || '[]');
570
  const product = products[selectedProductIndex];
571
- const cartItemId = `${product.name}-${color}`;
572
- const existingItem = cart.find(item => item.id === cartItemId);
573
-
574
- if (existingItem) {
575
- existingItem.quantity += quantity;
576
- } else {
577
- cart.push({
578
- id: cartItemId,
579
- name: product.name,
580
- price: product.price,
581
- photo: product.photos && product.photos.length > 0 ? product.photos[0] : '',
582
- quantity: quantity,
583
- color: color
584
- });
585
- }
586
 
587
  localStorage.setItem('cart', JSON.stringify(cart));
588
- closeModal('quantityModal');
589
  updateCartButton();
590
  }
591
 
@@ -600,7 +598,8 @@ def catalog():
600
  let total = 0;
601
 
602
  cartContent.innerHTML = cart.length === 0 ? '<p>Корзина пуста</p>' : cart.map(item => {
603
- const itemTotal = item.price * item.quantity;
 
604
  total += itemTotal;
605
  return `
606
  <div class="cart-item">
@@ -608,7 +607,9 @@ def catalog():
608
  ${item.photo ? `<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/${item.photo}" alt="${item.name}">` : ''}
609
  <div>
610
  <strong>${item.name}</strong>
611
- <p>${item.price} с × ${item.quantity} (Цвет: ${item.color})</p>
 
 
612
  </div>
613
  </div>
614
  <span>${itemTotal} с</span>
@@ -629,9 +630,12 @@ def catalog():
629
  let total = 0;
630
  let orderText = "Заказ:%0A";
631
  cart.forEach((item, index) => {
632
- const itemTotal = item.price * item.quantity;
 
633
  total += itemTotal;
634
- orderText += `${index + 1}. ${item.name} - ${item.price} с × ${item.quantity} (Цвет: ${item.color})%0A`;
 
 
635
  });
636
  orderText += `Итого: ${total} с`;
637
  window.open(`https://api.whatsapp.com/send?phone=996772179559&text=${orderText}`, '_blank');
@@ -674,7 +678,7 @@ def catalog():
674
  </body>
675
  </html>
676
  '''
677
- return render_template_string(catalog_html, products=products, categories=categories, repo_id=REPO_ID)
678
 
679
  @app.route('/product/<int:index>')
680
  def product_detail(index):
@@ -683,7 +687,7 @@ def product_detail(index):
683
  try:
684
  product = products[index]
685
  except IndexError:
686
- return "Продукт не найден", 404
687
  detail_html = '''
688
  <div class="container" style="padding: 20px;">
689
  <h2 style="font-size: 1.8rem; font-weight: 600; margin-bottom: 20px;">{{ product['name'] }}</h2>
@@ -712,7 +716,6 @@ def product_detail(index):
712
  <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
713
  <p><strong>Цена:</strong> {{ product['price'] }} с</p>
714
  <p><strong>Описание:</strong> {{ product['description'] }}</p>
715
- <p><strong>Доступные цвета:</strong> {{ product.get('colors', ['Нет цветов'])|join(', ') }}</p>
716
  </div>
717
  '''
718
  return render_template_string(detail_html, product=product, repo_id=REPO_ID)
@@ -722,6 +725,7 @@ def admin():
722
  data = load_data()
723
  products = data['products']
724
  categories = data['categories']
 
725
 
726
  if request.method == 'POST':
727
  action = request.form.get('action')
@@ -743,17 +747,39 @@ def admin():
743
  save_data(data)
744
  return redirect(url_for('admin'))
745
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
746
  elif action == 'add':
747
  name = request.form.get('name')
748
  price = request.form.get('price')
749
  description = request.form.get('description')
750
  category = request.form.get('category')
751
  photos_files = request.files.getlist('photos')
752
- colors = request.form.getlist('colors')
753
  photos_list = []
754
 
755
  if photos_files:
756
- for photo in photos_files[:10]: # Ограничение до 10 фото
757
  if photo and photo.filename:
758
  photo_filename = secure_filename(photo.filename)
759
  uploads_dir = 'uploads'
@@ -767,7 +793,7 @@ def admin():
767
  repo_id=REPO_ID,
768
  repo_type="dataset",
769
  token=HF_TOKEN_WRITE,
770
- commit_message=f"Добавлено фото для товара {name}"
771
  )
772
  photos_list.append(photo_filename)
773
  if os.path.exists(temp_path):
@@ -782,8 +808,7 @@ def admin():
782
  'price': price,
783
  'description': description,
784
  'category': category if category in categories else 'Без категории',
785
- 'photos': photos_list,
786
- 'colors': colors if colors else []
787
  }
788
  products.append(new_product)
789
  save_data(data)
@@ -796,11 +821,10 @@ def admin():
796
  description = request.form.get('description')
797
  category = request.form.get('category')
798
  photos_files = request.files.getlist('photos')
799
- colors = request.form.getlist('colors')
800
 
801
  if photos_files and any(photo.filename for photo in photos_files):
802
  new_photos_list = []
803
- for photo in photos_files[:10]: # Ограничение до 10 фото
804
  if photo and photo.filename:
805
  photo_filename = secure_filename(photo.filename)
806
  uploads_dir = 'uploads'
@@ -814,7 +838,7 @@ def admin():
814
  repo_id=REPO_ID,
815
  repo_type="dataset",
816
  token=HF_TOKEN_WRITE,
817
- commit_message=f"Обновлено фото для товара {name}"
818
  )
819
  new_photos_list.append(photo_filename)
820
  if os.path.exists(temp_path):
@@ -825,7 +849,6 @@ def admin():
825
  products[index]['price'] = float(price.replace(',', '.'))
826
  products[index]['description'] = description
827
  products[index]['category'] = category if category in categories else 'Без категории'
828
- products[index]['colors'] = colors if colors else []
829
  save_data(data)
830
  return redirect(url_for('admin'))
831
 
@@ -926,11 +949,11 @@ def admin():
926
  background-color: #dc2626;
927
  box-shadow: 0 4px 15px rgba(220, 38, 38, 0.4);
928
  }
929
- .product-list, .category-list {
930
  display: grid;
931
  gap: 20px;
932
  }
933
- .product-item, .category-item {
934
  background: #fff;
935
  padding: 20px;
936
  border-radius: 15px;
@@ -942,17 +965,6 @@ def admin():
942
  background: #f7fafc;
943
  border-radius: 10px;
944
  }
945
- .color-input-group {
946
- display: flex;
947
- gap: 10px;
948
- margin-top: 5px;
949
- }
950
- .add-color-btn {
951
- background-color: #10b981;
952
- }
953
- .add-color-btn:hover {
954
- background-color: #059669;
955
- }
956
  </style>
957
  </head>
958
  <body>
@@ -961,10 +973,10 @@ def admin():
961
  <img src="''' + LOGO_URL + '''" alt="Logo" class="header-logo">
962
  <h1>Админ-панель</h1>
963
  </div>
964
- <h1>Добавление товара</h1>
965
  <form method="POST" enctype="multipart/form-data">
966
  <input type="hidden" name="action" value="add">
967
- <label>Название товара:</label>
968
  <input type="text" name="name" required>
969
  <label>Цена:</label>
970
  <input type="number" name="price" step="0.01" required>
@@ -979,14 +991,7 @@ def admin():
979
  </select>
980
  <label>Фотографии (до 10):</label>
981
  <input type="file" name="photos" accept="image/*" multiple>
982
- <label>Цвета:</label>
983
- <div id="color-inputs">
984
- <div class="color-input-group">
985
- <input type="text" name="colors" placeholder="Например: Красный">
986
- </div>
987
- </div>
988
- <button type="button" class="add-color-btn" onclick="addColorInput()">Добавить цвет</button>
989
- <button type="submit">Добавить товар</button>
990
  </form>
991
 
992
  <h1>Управление категориями</h1>
@@ -1011,6 +1016,41 @@ def admin():
1011
  {% endfor %}
1012
  </div>
1013
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1014
  <h2>Управление базой данных</h2>
1015
  <form method="POST" action="{{ url_for('backup') }}" style="display: inline;">
1016
  <button type="submit">Создать копию</button>
@@ -1019,7 +1059,7 @@ def admin():
1019
  <button type="submit">Скачать базу</button>
1020
  </form>
1021
 
1022
- <h2>Список товаров</h2>
1023
  <div class="product-list">
1024
  {% for product in products %}
1025
  <div class="product-item">
@@ -1027,7 +1067,6 @@ def admin():
1027
  <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
1028
  <p><strong>Цена:</strong> {{ product['price'] }} с</p>
1029
  <p><strong>Описание:</strong> {{ product['description'] }}</p>
1030
- <p><strong>Цвета:</strong> {{ product.get('colors', ['Нет цветов'])|join(', ') }}</p>
1031
  {% if product.get('photos') and product['photos']|length > 0 %}
1032
  <div style="display: flex; flex-wrap: wrap; gap: 10px;">
1033
  {% for photo in product['photos'] %}
@@ -1057,15 +1096,6 @@ def admin():
1057
  </select>
1058
  <label>Фотографии (до 10):</label>
1059
  <input type="file" name="photos" accept="image/*" multiple>
1060
- <label>Цвета:</label>
1061
- <div id="edit-color-inputs-{{ loop.index0 }}">
1062
- {% for color in product.get('colors', []) %}
1063
- <div class="color-input-group">
1064
- <input type="text" name="colors" value="{{ color }}">
1065
- </div>
1066
- {% endfor %}
1067
- </div>
1068
- <button type="button" class="add-color-btn" onclick="addColorInput('edit-color-inputs-{{ loop.index0 }}')">Добавить цвет</button>
1069
  <button type="submit">Сохранить</button>
1070
  </form>
1071
  </details>
@@ -1078,19 +1108,10 @@ def admin():
1078
  {% endfor %}
1079
  </div>
1080
  </div>
1081
- <script>
1082
- function addColorInput(containerId = 'color-inputs') {
1083
- const container = document.getElementById(containerId);
1084
- const newInput = document.createElement('div');
1085
- newInput.className = 'color-input-group';
1086
- newInput.innerHTML = '<input type="text" name="colors" placeholder="Например: Красный">';
1087
- container.appendChild(newInput);
1088
- }
1089
- </script>
1090
  </body>
1091
  </html>
1092
  '''
1093
- return render_template_string(admin_html, products=products, categories=categories, repo_id=REPO_ID)
1094
 
1095
  @app.route('/backup', methods=['POST'])
1096
  def backup():
 
30
  data = json.load(file)
31
  logging.info("Данные успешно загружены из JSON")
32
  if not isinstance(data, dict) or 'products' not in data or 'categories' not in data:
33
+ return {'products': [], 'categories': [], 'options': []}
34
+ if 'options' not in data:
35
+ data['options'] = []
36
  return data
37
  except FileNotFoundError:
38
  logging.warning("Локальный файл базы данных не найден после скачивания.")
39
+ return {'products': [], 'categories': [], 'options': []}
40
  except json.JSONDecodeError:
41
  logging.error("Ошибка: Невозможно декодировать JSON файл.")
42
+ return {'products': [], 'categories': [], 'options': []}
43
  except RepositoryNotFoundError:
44
  logging.error("Репозиторий не найден. Создание локальной базы данных.")
45
+ return {'products': [], 'categories': [], 'options': []}
46
  except Exception as e:
47
  logging.error(f"Произошла ошибка при загрузке данных: {e}")
48
+ return {'products': [], 'categories': [], 'options': []}
49
 
50
  def save_data(data):
51
  try:
 
96
  time.sleep(800)
97
 
98
  @app.route('/')
99
+ def menu():
100
  data = load_data()
101
  products = data['products']
102
  categories = data['categories']
103
+ options = data['options']
104
 
105
+ menu_html = '''
106
  <!DOCTYPE html>
107
  <html lang="ru">
108
  <head>
109
  <meta charset="UTF-8">
110
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
111
+ <title>Routine wholesale - Меню</title>
112
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
113
  <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
114
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.css">
 
387
  border-radius: 8px;
388
  margin-right: 15px;
389
  }
390
+ .quantity-input {
391
  width: 100%;
392
  max-width: 150px;
393
  padding: 8px;
 
396
  font-size: 1rem;
397
  margin: 5px 0;
398
  }
399
+ .options-checkbox {
400
+ margin: 5px 0;
401
+ }
402
  .clear-cart {
403
  background-color: #ef4444;
404
  }
 
419
  <div class="container">
420
  <div class="header">
421
  <img src="''' + LOGO_URL + '''" alt="Logo" class="header-logo">
422
+ <h1>Меню</h1>
423
  <button class="theme-toggle" onclick="toggleTheme()">
424
  <i class="fas fa-moon"></i>
425
  </button>
 
431
  {% endfor %}
432
  </div>
433
  <div class="search-container">
434
+ <input type="text" id="search-input" placeholder="Поиск блюд...">
435
  </div>
436
  <div class="products-grid" id="products-grid">
437
  {% for product in products %}
 
450
  <div class="product-price">{{ product['price'] }} с</div>
451
  <p class="product-description">{{ product['description'][:50] }}{% if product['description']|length > 50 %}...{% endif %}</p>
452
  <button class="product-button" onclick="openModal({{ loop.index0 }})">Подробнее</button>
453
+ <button class="product-button add-to-cart" onclick="openOptionsModal({{ loop.index0 }})">В корзину</button>
454
  </div>
455
  {% endfor %}
456
  </div>
 
464
  </div>
465
  </div>
466
 
467
+ <!-- Options Modal -->
468
+ <div id="optionsModal" class="modal">
469
  <div class="modal-content">
470
+ <span class="close" onclick="closeModal('optionsModal')">×</span>
471
+ <h2>Укажите количество и дополнительные опции</h2>
472
  <input type="number" id="quantityInput" class="quantity-input" min="1" value="1">
473
+ <div id="optionsList"></div>
474
  <button class="product-button" onclick="confirmAddToCart()">Добавить</button>
475
  </div>
476
  </div>
 
496
  <script src="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.js"></script>
497
  <script>
498
  const products = {{ products|tojson }};
499
+ const options = {{ options|tojson }};
500
  let selectedProductIndex = null;
501
 
502
  function toggleTheme() {
 
543
  });
544
  }
545
 
546
+ function openOptionsModal(index) {
547
  selectedProductIndex = index;
548
  const product = products[index];
549
+ const optionsList = document.getElementById('optionsList');
550
+ optionsList.innerHTML = options.map(option => `
551
+ <label class="options-checkbox">
552
+ <input type="checkbox" class="option-checkbox" data-name="${option.name}" data-price="${option.price}">
553
+ ${option.name} (+${option.price} с)
554
+ </label>
555
+ `).join('');
556
+ document.getElementById('optionsModal').style.display = 'block';
 
 
 
 
 
 
 
 
557
  document.getElementById('quantityInput').value = 1;
558
  }
559
 
560
  function confirmAddToCart() {
561
  if (selectedProductIndex === null) return;
562
  const quantity = parseInt(document.getElementById('quantityInput').value) || 1;
 
563
  if (quantity <= 0) {
564
  alert("Укажите количество больше 0");
565
  return;
566
  }
567
  let cart = JSON.parse(localStorage.getItem('cart') || '[]');
568
  const product = products[selectedProductIndex];
569
+ const selectedOptions = Array.from(document.querySelectorAll('.option-checkbox:checked'))
570
+ .map(cb => ({
571
+ name: cb.dataset.name,
572
+ price: parseFloat(cb.dataset.price)
573
+ }));
574
+
575
+ const cartItemId = `${product.name}-${Date.now()}`; // Уникальный ID для каждого добавления
576
+ cart.push({
577
+ id: cartItemId,
578
+ name: product.name,
579
+ basePrice: product.price,
580
+ photo: product.photos && product.photos.length > 0 ? product.photos[0] : '',
581
+ quantity: quantity,
582
+ options: selectedOptions
583
+ });
584
 
585
  localStorage.setItem('cart', JSON.stringify(cart));
586
+ closeModal('optionsModal');
587
  updateCartButton();
588
  }
589
 
 
598
  let total = 0;
599
 
600
  cartContent.innerHTML = cart.length === 0 ? '<p>Корзина пуста</p>' : cart.map(item => {
601
+ const optionsTotal = item.options.reduce((sum, opt) => sum + opt.price, 0);
602
+ const itemTotal = (item.basePrice + optionsTotal) * item.quantity;
603
  total += itemTotal;
604
  return `
605
  <div class="cart-item">
 
607
  ${item.photo ? `<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/${item.photo}" alt="${item.name}">` : ''}
608
  <div>
609
  <strong>${item.name}</strong>
610
+ <p>${item.basePrice} с × ${item.quantity}${
611
+ item.options.length > 0 ? '<br>Опции: ' + item.options.map(o => `${o.name} (+${o.price} с)`).join(', ') : ''
612
+ }</p>
613
  </div>
614
  </div>
615
  <span>${itemTotal} с</span>
 
630
  let total = 0;
631
  let orderText = "Заказ:%0A";
632
  cart.forEach((item, index) => {
633
+ const optionsTotal = item.options.reduce((sum, opt) => sum + opt.price, 0);
634
+ const itemTotal = (item.basePrice + optionsTotal) * item.quantity;
635
  total += itemTotal;
636
+ orderText += `${index + 1}. ${item.name} - ${item.basePrice} с × ${item.quantity}${
637
+ item.options.length > 0 ? ' (' + item.options.map(o => `${o.name} +${o.price} с`).join(', ') + ')' : ''
638
+ }%0A`;
639
  });
640
  orderText += `Итого: ${total} с`;
641
  window.open(`https://api.whatsapp.com/send?phone=996772179559&text=${orderText}`, '_blank');
 
678
  </body>
679
  </html>
680
  '''
681
+ return render_template_string(menu_html, products=products, categories=categories, options=options, repo_id=REPO_ID)
682
 
683
  @app.route('/product/<int:index>')
684
  def product_detail(index):
 
687
  try:
688
  product = products[index]
689
  except IndexError:
690
+ return "Блюдо не найдено", 404
691
  detail_html = '''
692
  <div class="container" style="padding: 20px;">
693
  <h2 style="font-size: 1.8rem; font-weight: 600; margin-bottom: 20px;">{{ product['name'] }}</h2>
 
716
  <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
717
  <p><strong>Цена:</strong> {{ product['price'] }} с</p>
718
  <p><strong>Описание:</strong> {{ product['description'] }}</p>
 
719
  </div>
720
  '''
721
  return render_template_string(detail_html, product=product, repo_id=REPO_ID)
 
725
  data = load_data()
726
  products = data['products']
727
  categories = data['categories']
728
+ options = data['options']
729
 
730
  if request.method == 'POST':
731
  action = request.form.get('action')
 
747
  save_data(data)
748
  return redirect(url_for('admin'))
749
 
750
+ elif action == 'add_option':
751
+ option_name = request.form.get('option_name')
752
+ option_price = float(request.form.get('option_price', '0').replace(',', '.'))
753
+ if option_name and option_name not in [opt['name'] for opt in options]:
754
+ options.append({'name': option_name, 'price': option_price})
755
+ save_data(data)
756
+ return redirect(url_for('admin'))
757
+ return "Ошибка: Опция уже существует или не указано название", 400
758
+
759
+ elif action == 'delete_option':
760
+ option_index = int(request.form.get('option_index'))
761
+ del options[option_index]
762
+ save_data(data)
763
+ return redirect(url_for('admin'))
764
+
765
+ elif action == 'edit_option':
766
+ option_index = int(request.form.get('option_index'))
767
+ option_name = request.form.get('option_name')
768
+ option_price = float(request.form.get('option_price').replace(',', '.'))
769
+ options[option_index] = {'name': option_name, 'price': option_price}
770
+ save_data(data)
771
+ return redirect(url_for('admin'))
772
+
773
  elif action == 'add':
774
  name = request.form.get('name')
775
  price = request.form.get('price')
776
  description = request.form.get('description')
777
  category = request.form.get('category')
778
  photos_files = request.files.getlist('photos')
 
779
  photos_list = []
780
 
781
  if photos_files:
782
+ for photo in photos_files[:10]:
783
  if photo and photo.filename:
784
  photo_filename = secure_filename(photo.filename)
785
  uploads_dir = 'uploads'
 
793
  repo_id=REPO_ID,
794
  repo_type="dataset",
795
  token=HF_TOKEN_WRITE,
796
+ commit_message=f"Добавлено фото для блюда {name}"
797
  )
798
  photos_list.append(photo_filename)
799
  if os.path.exists(temp_path):
 
808
  'price': price,
809
  'description': description,
810
  'category': category if category in categories else 'Без категории',
811
+ 'photos': photos_list
 
812
  }
813
  products.append(new_product)
814
  save_data(data)
 
821
  description = request.form.get('description')
822
  category = request.form.get('category')
823
  photos_files = request.files.getlist('photos')
 
824
 
825
  if photos_files and any(photo.filename for photo in photos_files):
826
  new_photos_list = []
827
+ for photo in photos_files[:10]:
828
  if photo and photo.filename:
829
  photo_filename = secure_filename(photo.filename)
830
  uploads_dir = 'uploads'
 
838
  repo_id=REPO_ID,
839
  repo_type="dataset",
840
  token=HF_TOKEN_WRITE,
841
+ commit_message=f"Обновлено фото для блюда {name}"
842
  )
843
  new_photos_list.append(photo_filename)
844
  if os.path.exists(temp_path):
 
849
  products[index]['price'] = float(price.replace(',', '.'))
850
  products[index]['description'] = description
851
  products[index]['category'] = category if category in categories else 'Без категории'
 
852
  save_data(data)
853
  return redirect(url_for('admin'))
854
 
 
949
  background-color: #dc2626;
950
  box-shadow: 0 4px 15px rgba(220, 38, 38, 0.4);
951
  }
952
+ .product-list, .category-list, .options-list {
953
  display: grid;
954
  gap: 20px;
955
  }
956
+ .product-item, .category-item, .option-item {
957
  background: #fff;
958
  padding: 20px;
959
  border-radius: 15px;
 
965
  background: #f7fafc;
966
  border-radius: 10px;
967
  }
 
 
 
 
 
 
 
 
 
 
 
968
  </style>
969
  </head>
970
  <body>
 
973
  <img src="''' + LOGO_URL + '''" alt="Logo" class="header-logo">
974
  <h1>Админ-панель</h1>
975
  </div>
976
+ <h1>Добавление блюда</h1>
977
  <form method="POST" enctype="multipart/form-data">
978
  <input type="hidden" name="action" value="add">
979
+ <label>Название блюда:</label>
980
  <input type="text" name="name" required>
981
  <label>Цена:</label>
982
  <input type="number" name="price" step="0.01" required>
 
991
  </select>
992
  <label>Фотографии (до 10):</label>
993
  <input type="file" name="photos" accept="image/*" multiple>
994
+ <button type="submit">Добавить блюдо</button>
 
 
 
 
 
 
 
995
  </form>
996
 
997
  <h1>Управление категориями</h1>
 
1016
  {% endfor %}
1017
  </div>
1018
 
1019
+ <h1>Управление опциями</h1>
1020
+ <form method="POST">
1021
+ <input type="hidden" name="action" value="add_option">
1022
+ <label>Название опции:</label>
1023
+ <input type="text" name="option_name" required>
1024
+ <label>Дополнительная стоимость:</label>
1025
+ <input type="number" name="option_price" step="0.01" value="0">
1026
+ <button type="submit">Добавить</button>
1027
+ </form>
1028
+
1029
+ <h2>Список опций</h2>
1030
+ <div class="options-list">
1031
+ {% for option in options %}
1032
+ <div class="option-item">
1033
+ <details>
1034
+ <summary>{{ option['name'] }} ({{ option['price'] }} с)</summary>
1035
+ <form method="POST" class="edit-form">
1036
+ <input type="hidden" name="action" value="edit_option">
1037
+ <input type="hidden" name="option_index" value="{{ loop.index0 }}">
1038
+ <label>Название:</label>
1039
+ <input type="text" name="option_name" value="{{ option['name'] }}" required>
1040
+ <label>Стоимость:</label>
1041
+ <input type="number" name="option_price" step="0.01" value="{{ option['price'] }}" required>
1042
+ <button type="submit">Сохранить</button>
1043
+ </form>
1044
+ </details>
1045
+ <form method="POST" style="display: inline;">
1046
+ <input type="hidden" name="action" value="delete_option">
1047
+ <input type="hidden" name="option_index" value="{{ loop.index0 }}">
1048
+ <button type="submit" class="delete-button">Удалить</button>
1049
+ </form>
1050
+ </div>
1051
+ {% endfor %}
1052
+ </div>
1053
+
1054
  <h2>Управление базой данных</h2>
1055
  <form method="POST" action="{{ url_for('backup') }}" style="display: inline;">
1056
  <button type="submit">Создать копию</button>
 
1059
  <button type="submit">Скачать базу</button>
1060
  </form>
1061
 
1062
+ <h2>Список блюд</h2>
1063
  <div class="product-list">
1064
  {% for product in products %}
1065
  <div class="product-item">
 
1067
  <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
1068
  <p><strong>Цена:</strong> {{ product['price'] }} с</p>
1069
  <p><strong>Описание:</strong> {{ product['description'] }}</p>
 
1070
  {% if product.get('photos') and product['photos']|length > 0 %}
1071
  <div style="display: flex; flex-wrap: wrap; gap: 10px;">
1072
  {% for photo in product['photos'] %}
 
1096
  </select>
1097
  <label>Фотографии (до 10):</label>
1098
  <input type="file" name="photos" accept="image/*" multiple>
 
 
 
 
 
 
 
 
 
1099
  <button type="submit">Сохранить</button>
1100
  </form>
1101
  </details>
 
1108
  {% endfor %}
1109
  </div>
1110
  </div>
 
 
 
 
 
 
 
 
 
1111
  </body>
1112
  </html>
1113
  '''
1114
+ return render_template_string(admin_html, products=products, categories=categories, options=options, repo_id=REPO_ID)
1115
 
1116
  @app.route('/backup', methods=['POST'])
1117
  def backup():