Kgshop commited on
Commit
31f003a
·
verified ·
1 Parent(s): 67ecbac

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +570 -200
app.py CHANGED
@@ -1,3 +1,5 @@
 
 
1
  from flask import Flask, render_template_string, request, redirect, url_for
2
  import json
3
  import os
@@ -95,7 +97,7 @@ def catalog():
95
  data = load_data()
96
  products = sorted(data['products'], key=lambda x: x.get('added_at', ''), reverse=True)
97
  categories = data['categories']
98
-
99
  catalog_html = '''
100
  <!DOCTYPE html>
101
  <html lang="ru">
@@ -108,12 +110,15 @@ def catalog():
108
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.css">
109
  <style>
110
  :root {
111
- --primary-color: #F48FB1;
112
- --secondary-color: #CE93D8;
113
- --text-color: #333;
114
- --bg-color: #FCE4EC;
115
- --light-text: #fff;
116
  --shadow-color: rgba(0, 0, 0, 0.1);
 
 
 
117
  }
118
  * {
119
  margin: 0;
@@ -181,7 +186,7 @@ def catalog():
181
  }
182
  #search-input:focus {
183
  border-color: var(--primary-color);
184
- box-shadow: 0 4px 15px rgba(244, 143, 177, 0.3);
185
  }
186
  .category-filter {
187
  padding: 8px 16px;
@@ -198,7 +203,7 @@ def catalog():
198
  background-color: var(--primary-color);
199
  color: var(--light-text);
200
  border-color: var(--primary-color);
201
- box-shadow: 0 2px 10px rgba(244, 143, 177, 0.4);
202
  }
203
  .products-grid {
204
  display: grid;
@@ -248,7 +253,7 @@ def catalog():
248
  }
249
  .product-price {
250
  font-size: 1.1rem;
251
- color: #ef4444;
252
  font-weight: 700;
253
  text-align: center;
254
  margin: 5px 0;
@@ -279,22 +284,22 @@ def catalog():
279
  text-decoration: none;
280
  }
281
  .product-button:hover {
282
- background-color: #E91E63;
283
- box-shadow: 0 4px 15px rgba(233, 30, 99, 0.4);
284
  transform: translateY(-2px);
285
  }
286
  .add-to-cart {
287
  background-color: var(--secondary-color);
288
  }
289
  .add-to-cart:hover {
290
- background-color: #BA68C8;
291
- box-shadow: 0 4px 15px rgba(186, 104, 200, 0.4);
292
  }
293
  #cart-button {
294
  position: fixed;
295
  bottom: 20px;
296
  right: 20px;
297
- background-color: #ef4444;
298
  color: var(--light-text);
299
  border: none;
300
  border-radius: 50%;
@@ -303,10 +308,13 @@ def catalog():
303
  font-size: 1.2rem;
304
  cursor: pointer;
305
  display: none;
306
- box-shadow: 0 4px 15px rgba(239, 68, 68, 0.4);
307
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
308
  z-index: 1000;
309
  }
 
 
 
310
  .modal {
311
  display: none;
312
  position: fixed;
@@ -366,7 +374,7 @@ def catalog():
366
  margin: 5px 0;
367
  }
368
  .clear-cart {
369
- background-color: #ef4444;
370
  }
371
  .clear-cart:hover {
372
  background-color: #dc2626;
@@ -376,13 +384,66 @@ def catalog():
376
  background-color: var(--secondary-color);
377
  }
378
  .order-button:hover {
379
- background-color: #BA68C8;
380
- box-shadow: 0 4px 15px rgba(186, 104, 200, 0.4);
381
  }
382
  @media (max-width: 768px) {
383
  .products-grid {
384
  grid-template-columns: repeat(2, minmax(150px, 1fr));
385
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
386
  }
387
  </style>
388
  </head>
@@ -403,14 +464,14 @@ def catalog():
403
  </div>
404
  <div class="products-grid" id="products-grid">
405
  {% for product in products %}
406
- <div class="product"
407
- data-name="{{ product['name']|lower }}"
408
  data-description="{{ product['description']|lower }}"
409
  data-category="{{ product.get('category', 'Без категории') }}">
410
  {% if product.get('photos') and product['photos']|length > 0 %}
411
  <div class="product-image">
412
- <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product['photos'][0] }}"
413
- alt="{{ product['name'] }}"
414
  loading="lazy">
415
  </div>
416
  {% endif %}
@@ -454,7 +515,7 @@ def catalog():
454
  </div>
455
  </div>
456
 
457
- <button id="cart-button" onclick="openCartModal()">🛒</button>
458
 
459
  <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
460
  <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.5.3/dist/umd/popper.min.js"></script>
@@ -549,7 +610,9 @@ def catalog():
549
 
550
  function updateCartButton() {
551
  const cart = JSON.parse(localStorage.getItem('cart') || '[]');
552
- document.getElementById('cart-button').style.display = cart.length > 0 ? 'block' : 'none';
 
 
553
  }
554
 
555
  function openCartModal() {
@@ -602,7 +665,9 @@ def catalog():
602
  }
603
 
604
  window.onclick = function(event) {
605
- if (event.target.className === 'modal') event.target.style.display = "none";
 
 
606
  }
607
 
608
  document.getElementById('search-input').addEventListener('input', filterProducts);
@@ -627,7 +692,7 @@ def catalog():
627
  });
628
  }
629
 
630
- updateCartButton();
631
  </script>
632
  </body>
633
  </html>
@@ -642,24 +707,25 @@ def product_detail(index):
642
  product = products[index]
643
  except IndexError:
644
  return "Продукт не найден", 404
 
645
  detail_html = '''
646
- <div class="container" style="padding: 20px;">
647
- <h2 style="font-size: 1.8rem; font-weight: 600; margin-bottom: 20px;">{{ product['name'] }}</h2>
648
- <div class="swiper-container" style="max-width: 400px; margin: 0 auto 20px;">
649
  <div class="swiper-wrapper">
650
- {% if product.get('photos') %}
651
  {% for photo in product['photos'] %}
652
- <div class="swiper-slide" style="background-color: #fff; display: flex; justify-content: center; align-items: center;">
653
  <div class="swiper-zoom-container">
654
- <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo }}"
655
- alt="{{ product['name'] }}"
656
- style="max-width: 100%; max-height: 300px; object-fit: contain;">
657
  </div>
658
  </div>
659
  {% endfor %}
660
  {% else %}
661
- <div class="swiper-slide">
662
- <img src="https://via.placeholder.com/300" alt="No Image">
663
  </div>
664
  {% endif %}
665
  </div>
@@ -667,14 +733,17 @@ def product_detail(index):
667
  <div class="swiper-button-next"></div>
668
  <div class="swiper-button-prev"></div>
669
  </div>
670
- <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
671
- <p><strong>Цена:</strong> {{ product['price'] }} с</p>
672
- <p><strong>Описание:</strong> {{ product['description'] }}</p>
673
- <p><strong>Доступные цвета:</strong> {{ product.get('colors', ['Нет цветов'])|join(', ') }}</p>
 
 
674
  </div>
675
  '''
676
  return render_template_string(detail_html, product=product, repo_id=REPO_ID)
677
 
 
678
  @app.route('/admin', methods=['GET', 'POST'])
679
  def admin():
680
  data = load_data()
@@ -683,7 +752,7 @@ def admin():
683
 
684
  if request.method == 'POST':
685
  action = request.form.get('action')
686
-
687
  if action == 'add_category':
688
  category_name = request.form.get('category_name')
689
  if category_name and category_name not in categories:
@@ -707,93 +776,139 @@ def admin():
707
  description = request.form.get('description')
708
  category = request.form.get('category')
709
  photos_files = request.files.getlist('photos')
710
- colors = request.form.getlist('colors')
711
  photos_list = []
712
-
713
  if photos_files:
714
- for photo in photos_files[:10]:
715
  if photo and photo.filename:
716
- photo_filename = secure_filename(photo.filename)
717
  uploads_dir = 'uploads'
718
  os.makedirs(uploads_dir, exist_ok=True)
719
  temp_path = os.path.join(uploads_dir, photo_filename)
720
- photo.save(temp_path)
721
- api = HfApi()
722
- api.upload_file(
723
- path_or_fileobj=temp_path,
724
- path_in_repo=f"photos/{photo_filename}",
725
- repo_id=REPO_ID,
726
- repo_type="dataset",
727
- token=HF_TOKEN_WRITE,
728
- commit_message=f"Добавлено фото для товара {name}"
729
- )
730
- photos_list.append(photo_filename)
731
- if os.path.exists(temp_path):
732
- os.remove(temp_path)
733
-
 
 
 
 
 
 
 
 
734
  if not name or not price or not description:
735
  return "Ошибка: Заполните все обязательные поля", 400
736
-
737
- price = float(price.replace(',', '.'))
 
 
 
 
738
  new_product = {
739
  'name': name,
740
  'price': price,
741
  'description': description,
742
  'category': category if category in categories else 'Без категории',
743
  'photos': photos_list,
744
- 'colors': colors if colors else [],
745
  'added_at': datetime.now().isoformat()
746
  }
747
  products.append(new_product)
748
  save_data(data)
749
  return redirect(url_for('admin'))
750
-
751
  elif action == 'edit':
752
- index = int(request.form.get('index'))
 
 
 
 
 
 
753
  name = request.form.get('name')
754
  price = request.form.get('price')
755
  description = request.form.get('description')
756
  category = request.form.get('category')
757
  photos_files = request.files.getlist('photos')
758
- colors = request.form.getlist('colors')
759
-
 
 
 
 
 
 
 
 
 
760
  if photos_files and any(photo.filename for photo in photos_files):
761
  new_photos_list = []
762
- for photo in photos_files[:10]:
763
  if photo and photo.filename:
764
- photo_filename = secure_filename(photo.filename)
765
  uploads_dir = 'uploads'
766
  os.makedirs(uploads_dir, exist_ok=True)
767
  temp_path = os.path.join(uploads_dir, photo_filename)
768
- photo.save(temp_path)
769
- api = HfApi()
770
- api.upload_file(
771
- path_or_fileobj=temp_path,
772
- path_in_repo=f"photos/{photo_filename}",
773
- repo_id=REPO_ID,
774
- repo_type="dataset",
775
- token=HF_TOKEN_WRITE,
776
- commit_message=f"Обновлено фото для товара {name}"
777
- )
778
- new_photos_list.append(photo_filename)
779
- if os.path.exists(temp_path):
780
- os.remove(temp_path)
781
- products[index]['photos'] = new_photos_list
782
-
 
 
 
 
 
 
 
 
 
783
  products[index]['name'] = name
784
- products[index]['price'] = float(price.replace(',', '.'))
785
  products[index]['description'] = description
786
  products[index]['category'] = category if category in categories else 'Без категории'
787
- products[index]['colors'] = colors if colors else []
 
788
  save_data(data)
789
  return redirect(url_for('admin'))
790
-
791
  elif action == 'delete':
792
- index = int(request.form.get('index'))
793
- del products[index]
794
- save_data(data)
 
 
 
 
 
 
 
 
 
795
  return redirect(url_for('admin'))
796
-
797
  admin_html = '''
798
  <!DOCTYPE html>
799
  <html lang="ru">
@@ -802,20 +917,28 @@ def admin():
802
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
803
  <title>Админ-панель</title>
804
  <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
 
805
  <style>
806
- :root {
807
- --primary-color: #F48FB1;
808
- --secondary-color: #CE93D8;
809
- --text-color: #333;
810
- --bg-color: #FCE4EC;
811
- --light-text: #fff;
812
  --shadow-color: rgba(0, 0, 0, 0.1);
 
 
 
 
 
 
813
  }
814
  body {
815
  font-family: 'Poppins', sans-serif;
816
  background: var(--bg-color);
817
  color: var(--text-color);
818
  padding: 20px;
 
819
  }
820
  .container {
821
  max-width: 1200px;
@@ -826,6 +949,7 @@ def admin():
826
  align-items: center;
827
  padding: 15px 0;
828
  border-bottom: 1px solid var(--primary-color);
 
829
  }
830
  .header-logo {
831
  width: 60px;
@@ -842,12 +966,16 @@ def admin():
842
  }
843
  h1, h2 {
844
  font-weight: 600;
 
845
  margin-bottom: 20px;
846
  color: var(--primary-color);
847
  }
 
 
 
848
  form {
849
  background: var(--light-text);
850
- padding: 20px;
851
  border-radius: 15px;
852
  box-shadow: 0 4px 15px var(--shadow-color);
853
  margin-bottom: 30px;
@@ -856,71 +984,232 @@ def admin():
856
  font-weight: 500;
857
  margin-top: 15px;
858
  display: block;
 
 
859
  }
860
- input, textarea, select {
 
 
 
 
861
  width: 100%;
862
  padding: 12px;
863
- margin-top: 5px;
864
  border: 1px solid var(--secondary-color);
865
  border-radius: 8px;
866
  font-size: 1rem;
867
  transition: all 0.3s ease;
 
868
  }
869
  input:focus, textarea:focus, select:focus {
870
  border-color: var(--primary-color);
871
- box-shadow: 0 0 5px rgba(244, 143, 177, 0.3);
872
  outline: none;
873
  }
874
- button {
875
- padding: 12px 20px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
876
  border: none;
877
  border-radius: 8px;
878
  background-color: var(--primary-color);
879
  color: var(--light-text);
880
- font-weight: 500;
 
881
  cursor: pointer;
882
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
883
- margin-top: 15px;
 
884
  }
885
- button:hover {
886
- background-color: #E91E63;
887
- box-shadow: 0 4px 15px rgba(233, 30, 99, 0.4);
888
  transform: translateY(-2px);
889
  }
890
  .delete-button {
891
- background-color: #ef4444;
892
  }
893
  .delete-button:hover {
894
- background-color: #dc2626;
895
  box-shadow: 0 4px 15px rgba(220, 38, 38, 0.4);
 
 
 
 
 
 
 
 
 
896
  }
897
  .product-list, .category-list {
898
  display: grid;
899
- gap: 20px;
900
  }
901
  .product-item, .category-item {
902
  background: var(--light-text);
903
- padding: 20px;
904
  border-radius: 15px;
905
  box-shadow: 0 4px 15px var(--shadow-color);
906
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
907
  .edit-form {
908
  margin-top: 15px;
909
- padding: 15px;
910
- background: #f7fafc;
911
  border-radius: 10px;
 
 
 
 
 
912
  }
913
  .color-input-group {
914
  display: flex;
915
  gap: 10px;
916
- margin-top: 5px;
917
- }
918
- .add-color-btn {
919
- background-color: var(--secondary-color);
920
- }
921
- .add-color-btn:hover {
922
- background-color: #BA68C8;
923
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
924
  </style>
925
  </head>
926
  <body>
@@ -929,43 +1218,51 @@ def admin():
929
  <img src="''' + LOGO_URL + '''" alt="Logo" class="header-logo">
930
  <h1>Админ-панель</h1>
931
  </div>
932
- <h1>Добавление товара</h1>
 
933
  <form method="POST" enctype="multipart/form-data">
934
  <input type="hidden" name="action" value="add">
935
- <label>Название товара:</label>
936
- <input type="text" name="name" required>
937
- <label>Цена:</label>
938
- <input type="number" name="price" step="0.01" required>
939
- <label>Описание:</label>
940
- <textarea name="description" rows="4" required></textarea>
941
- <label>Категория:</label>
942
- <select name="category">
 
 
 
943
  <option value="Без категории">Без категории</option>
944
  {% for category in categories %}
945
  <option value="{{ category }}">{{ category }}</option>
946
  {% endfor %}
947
  </select>
948
- <label>Фотографии (до 10):</label>
949
- <input type="file" name="photos" accept="image/*" multiple>
950
- <label>Цвета:</label>
951
- <div id="color-inputs">
 
 
952
  <div class="color-input-group">
953
  <input type="text" name="colors" placeholder="Например: Красный">
 
954
  </div>
955
  </div>
956
- <button type="button" class="add-color-btn" onclick="addColorInput()">Добавить цвет</button>
957
- <button type="submit">Добавить товар</button>
 
958
  </form>
959
 
960
- <h1>Управление категориями</h1>
961
  <form method="POST">
962
  <input type="hidden" name="action" value="add_category">
963
- <label>Название категории:</label>
964
- <input type="text" name="category_name" required>
965
- <button type="submit">Добавить</button>
966
  </form>
967
 
968
- <h2>Список категорий</h2>
969
  <div class="category-list">
970
  {% for category in categories %}
971
  <div class="category-item">
@@ -973,19 +1270,23 @@ def admin():
973
  <form method="POST" style="display: inline;">
974
  <input type="hidden" name="action" value="delete_category">
975
  <input type="hidden" name="category_index" value="{{ loop.index0 }}">
976
- <button type="submit" class="delete-button">Удалить</button>
977
  </form>
978
  </div>
 
 
979
  {% endfor %}
980
  </div>
981
 
982
  <h2>Управление базой данных</h2>
983
- <form method="POST" action="{{ url_for('backup') }}" style="display: inline;">
984
- <button type="submit">Создать копию</button>
985
- </form>
986
- <form method="GET" action="{{ url_for('download') }}" style="display: inline;">
987
- <button type="submit">Скачать базу</button>
988
- </form>
 
 
989
 
990
  <h2>Список товаров</h2>
991
  <div class="product-list">
@@ -995,65 +1296,107 @@ def admin():
995
  <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
996
  <p><strong>Цена:</strong> {{ product['price'] }} с</p>
997
  <p><strong>Описание:</strong> {{ product['description'] }}</p>
998
- <p><strong>Цвета:</strong> {{ product.get('colors', ['Нет цветов'])|join(', ') }}</p>
999
  {% if product.get('photos') and product['photos']|length > 0 %}
1000
- <div style="display: flex; flex-wrap: wrap; gap: 10px;">
1001
  {% for photo in product['photos'] %}
1002
- <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo }}"
1003
- alt="{{ product['name'] }}"
1004
- style="max-width: 100px; border-radius: 10px;">
1005
  {% endfor %}
1006
  </div>
1007
  {% endif %}
1008
- <details>
1009
- <summary>Редактировать</summary>
1010
- <form method="POST" enctype="multipart/form-data" class="edit-form">
1011
- <input type="hidden" name="action" value="edit">
1012
- <input type="hidden" name="index" value="{{ loop.index0 }}">
1013
- <label>Название:</label>
1014
- <input type="text" name="name" value="{{ product['name'] }}" required>
1015
- <label>Цена:</label>
1016
- <input type="number" name="price" step="0.01" value="{{ product['price'] }}" required>
1017
- <label>Описание:</label>
1018
- <textarea name="description" rows="4" required>{{ product['description'] }}</textarea>
1019
- <label>Категория:</label>
1020
- <select name="category">
1021
- <option value="Без категории" {% if product.get('category', 'Без категории') == 'Без категории' %}selected{% endif %}>Без категории</option>
1022
- {% for category in categories %}
1023
- <option value="{{ category }}" {% if product.get('category') == category %}selected{% endif %}>{{ category }}</option>
1024
- {% endfor %}
1025
- </select>
1026
- <label>Фотографии (до 10):</label>
1027
- <input type="file" name="photos" accept="image/*" multiple>
1028
- <label>Цвета:</label>
1029
- <div id="edit-color-inputs-{{ loop.index0 }}">
1030
- {% for color in product.get('colors', []) %}
1031
- <div class="color-input-group">
1032
- <input type="text" name="colors" value="{{ color }}">
 
 
 
 
 
 
 
 
 
 
 
 
1033
  </div>
1034
- {% endfor %}
1035
- </div>
1036
- <button type="button" class="add-color-btn" onclick="addColorInput('edit-color-inputs-{{ loop.index0 }}')">Добавить цвет</button>
1037
- <button type="submit">Сохранить</button>
 
 
 
 
1038
  </form>
1039
- </details>
1040
- <form method="POST">
1041
- <input type="hidden" name="action" value="delete">
1042
- <input type="hidden" name="index" value="{{ loop.index0 }}">
1043
- <button type="submit" class="delete-button">Удалить</button>
1044
- </form>
1045
  </div>
 
 
1046
  {% endfor %}
1047
  </div>
1048
  </div>
1049
  <script>
1050
- function addColorInput(containerId = 'color-inputs') {
1051
  const container = document.getElementById(containerId);
1052
- const newInput = document.createElement('div');
1053
- newInput.className = 'color-input-group';
1054
- newInput.innerHTML = '<input type="text" name="colors" placeholder="Например: Красный">';
1055
- container.appendChild(newInput);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1056
  }
 
 
 
 
 
 
 
 
 
1057
  </script>
1058
  </body>
1059
  </html>
@@ -1062,19 +1405,46 @@ def admin():
1062
 
1063
  @app.route('/backup', methods=['POST'])
1064
  def backup():
1065
- upload_db_to_hf()
1066
- return "Резервная копия создана.", 200
 
 
 
 
 
1067
 
1068
  @app.route('/download', methods=['GET'])
1069
  def download():
1070
- download_db_from_hf()
1071
- return "База данных скачана.", 200
 
 
 
 
 
 
 
 
 
 
1072
 
1073
  if __name__ == '__main__':
 
 
 
 
1074
  backup_thread = threading.Thread(target=periodic_backup, daemon=True)
1075
  backup_thread.start()
 
 
1076
  try:
1077
  load_data()
1078
  except Exception as e:
1079
- logging.error(f"Не удалось загрузить базу данных: {e}")
1080
- app.run(debug=True, host='0.0.0.0', port=7860)
 
 
 
 
 
 
 
1
+ # --- START OF FILE app (16) (10).py ---
2
+
3
  from flask import Flask, render_template_string, request, redirect, url_for
4
  import json
5
  import os
 
97
  data = load_data()
98
  products = sorted(data['products'], key=lambda x: x.get('added_at', ''), reverse=True)
99
  categories = data['categories']
100
+
101
  catalog_html = '''
102
  <!DOCTYPE html>
103
  <html lang="ru">
 
110
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.css">
111
  <style>
112
  :root {
113
+ --primary-color: #FFA726; /* Orange/Amber */
114
+ --secondary-color: #FFCA28; /* Lighter Orange/Amber */
115
+ --text-color: #333; /* Dark Gray */
116
+ --bg-color: #FFF8E1; /* Very Light Yellow/Amber */
117
+ --light-text: #fff; /* White */
118
  --shadow-color: rgba(0, 0, 0, 0.1);
119
+ --accent-color: #D84315; /* Deep Orange for price */
120
+ --hover-primary-color: #FB8C00; /* Darker Orange */
121
+ --hover-secondary-color: #FFA000; /* Darker Amber */
122
  }
123
  * {
124
  margin: 0;
 
186
  }
187
  #search-input:focus {
188
  border-color: var(--primary-color);
189
+ box-shadow: 0 4px 15px rgba(255, 167, 38, 0.3); /* Adjusted shadow color */
190
  }
191
  .category-filter {
192
  padding: 8px 16px;
 
203
  background-color: var(--primary-color);
204
  color: var(--light-text);
205
  border-color: var(--primary-color);
206
+ box-shadow: 0 2px 10px rgba(255, 167, 38, 0.4); /* Adjusted shadow color */
207
  }
208
  .products-grid {
209
  display: grid;
 
253
  }
254
  .product-price {
255
  font-size: 1.1rem;
256
+ color: var(--accent-color); /* Changed price color */
257
  font-weight: 700;
258
  text-align: center;
259
  margin: 5px 0;
 
284
  text-decoration: none;
285
  }
286
  .product-button:hover {
287
+ background-color: var(--hover-primary-color); /* Darker hover */
288
+ box-shadow: 0 4px 15px rgba(251, 140, 0, 0.4); /* Adjusted shadow */
289
  transform: translateY(-2px);
290
  }
291
  .add-to-cart {
292
  background-color: var(--secondary-color);
293
  }
294
  .add-to-cart:hover {
295
+ background-color: var(--hover-secondary-color); /* Darker hover */
296
+ box-shadow: 0 4px 15px rgba(255, 160, 0, 0.4); /* Adjusted shadow */
297
  }
298
  #cart-button {
299
  position: fixed;
300
  bottom: 20px;
301
  right: 20px;
302
+ background-color: var(--primary-color); /* Changed from red */
303
  color: var(--light-text);
304
  border: none;
305
  border-radius: 50%;
 
308
  font-size: 1.2rem;
309
  cursor: pointer;
310
  display: none;
311
+ box-shadow: 0 4px 15px rgba(255, 167, 38, 0.4); /* Adjusted shadow */
312
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
313
  z-index: 1000;
314
  }
315
+ #cart-button:hover {
316
+ background-color: var(--hover-primary-color);
317
+ }
318
  .modal {
319
  display: none;
320
  position: fixed;
 
374
  margin: 5px 0;
375
  }
376
  .clear-cart {
377
+ background-color: #ef4444; /* Kept red for destructive action */
378
  }
379
  .clear-cart:hover {
380
  background-color: #dc2626;
 
384
  background-color: var(--secondary-color);
385
  }
386
  .order-button:hover {
387
+ background-color: var(--hover-secondary-color); /* Darker hover */
388
+ box-shadow: 0 4px 15px rgba(255, 160, 0, 0.4); /* Adjusted shadow */
389
  }
390
  @media (max-width: 768px) {
391
  .products-grid {
392
  grid-template-columns: repeat(2, minmax(150px, 1fr));
393
  }
394
+ .header h1 {
395
+ font-size: 1.2rem; /* Slightly smaller on mobile */
396
+ }
397
+ #search-input {
398
+ padding: 10px 15px;
399
+ }
400
+ .category-filter {
401
+ padding: 6px 12px;
402
+ font-size: 0.8rem;
403
+ }
404
+ .product h2 {
405
+ font-size: 0.9rem;
406
+ }
407
+ .product-price {
408
+ font-size: 1rem;
409
+ }
410
+ .product-description {
411
+ font-size: 0.75rem;
412
+ }
413
+ .product-button {
414
+ padding: 6px;
415
+ font-size: 0.75rem;
416
+ }
417
+ #cart-button {
418
+ width: 45px;
419
+ height: 45px;
420
+ font-size: 1.1rem;
421
+ }
422
+ .modal-content {
423
+ padding: 15px;
424
+ }
425
+ }
426
+ @media (max-width: 480px) {
427
+ .products-grid {
428
+ grid-template-columns: 1fr; /* Single column on very small screens */
429
+ gap: 10px;
430
+ }
431
+ .header {
432
+ flex-direction: column;
433
+ align-items: center;
434
+ text-align: center;
435
+ }
436
+ .header h1 {
437
+ margin-left: 0;
438
+ margin-top: 10px;
439
+ }
440
+ .filters-container {
441
+ justify-content: flex-start; /* Align filters left */
442
+ gap: 5px;
443
+ }
444
+ .category-filter {
445
+ padding: 5px 10px;
446
+ }
447
  }
448
  </style>
449
  </head>
 
464
  </div>
465
  <div class="products-grid" id="products-grid">
466
  {% for product in products %}
467
+ <div class="product"
468
+ data-name="{{ product['name']|lower }}"
469
  data-description="{{ product['description']|lower }}"
470
  data-category="{{ product.get('category', 'Без категории') }}">
471
  {% if product.get('photos') and product['photos']|length > 0 %}
472
  <div class="product-image">
473
+ <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product['photos'][0] }}"
474
+ alt="{{ product['name'] }}"
475
  loading="lazy">
476
  </div>
477
  {% endif %}
 
515
  </div>
516
  </div>
517
 
518
+ <button id="cart-button" onclick="openCartModal()"><i class="fas fa-shopping-cart"></i></button> <!-- Changed text to icon -->
519
 
520
  <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
521
  <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.5.3/dist/umd/popper.min.js"></script>
 
610
 
611
  function updateCartButton() {
612
  const cart = JSON.parse(localStorage.getItem('cart') || '[]');
613
+ document.getElementById('cart-button').style.display = cart.length > 0 ? 'flex' : 'none'; // Changed to flex for icon centering
614
+ document.getElementById('cart-button').style.alignItems = 'center';
615
+ document.getElementById('cart-button').style.justifyContent = 'center';
616
  }
617
 
618
  function openCartModal() {
 
665
  }
666
 
667
  window.onclick = function(event) {
668
+ if (event.target.classList.contains('modal')) { // Improved modal closing
669
+ event.target.style.display = "none";
670
+ }
671
  }
672
 
673
  document.getElementById('search-input').addEventListener('input', filterProducts);
 
692
  });
693
  }
694
 
695
+ updateCartButton(); // Initial check
696
  </script>
697
  </body>
698
  </html>
 
707
  product = products[index]
708
  except IndexError:
709
  return "Продукт не найден", 404
710
+ # Используем ту же цветовую гамму для детальной страницы
711
  detail_html = '''
712
+ <div class="container" style="padding: 20px; font-family: 'Poppins', sans-serif; color: #333;">
713
+ <h2 style="font-size: 1.8rem; font-weight: 600; margin-bottom: 20px; color: #FFA726;">{{ product['name'] }}</h2>
714
+ <div class="swiper-container" style="max-width: 400px; margin: 0 auto 20px; --swiper-navigation-color: #FFA726; --swiper-pagination-color: #FFA726;">
715
  <div class="swiper-wrapper">
716
+ {% if product.get('photos') and product['photos']|length > 0 %}
717
  {% for photo in product['photos'] %}
718
+ <div class="swiper-slide" style="background-color: #fff; display: flex; justify-content: center; align-items: center; border-radius: 10px;">
719
  <div class="swiper-zoom-container">
720
+ <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo }}"
721
+ alt="{{ product['name'] }}"
722
+ style="max-width: 100%; max-height: 350px; object-fit: contain;">
723
  </div>
724
  </div>
725
  {% endfor %}
726
  {% else %}
727
+ <div class="swiper-slide" style="background-color: #eee; display: flex; justify-content: center; align-items: center; border-radius: 10px; height: 350px;">
728
+ <span style="color: #999;">Нет изображения</span>
729
  </div>
730
  {% endif %}
731
  </div>
 
733
  <div class="swiper-button-next"></div>
734
  <div class="swiper-button-prev"></div>
735
  </div>
736
+ <div style="background: #FFF8E1; padding: 15px; border-radius: 10px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
737
+ <p style="margin-bottom: 10px;"><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
738
+ <p style="margin-bottom: 10px;"><strong>Цена:</strong> <span style="color: #D84315; font-weight: bold;">{{ product['price'] }} с</span></p>
739
+ <p style="margin-bottom: 10px;"><strong>Описание:</strong> {{ product['description'] }}</p>
740
+ <p><strong>Доступные цвета:</strong> {{ product.get('colors', ['Нет цветов'])|join(', ') if product.get('colors') else 'Нет цветов' }}</p>
741
+ </div>
742
  </div>
743
  '''
744
  return render_template_string(detail_html, product=product, repo_id=REPO_ID)
745
 
746
+
747
  @app.route('/admin', methods=['GET', 'POST'])
748
  def admin():
749
  data = load_data()
 
752
 
753
  if request.method == 'POST':
754
  action = request.form.get('action')
755
+
756
  if action == 'add_category':
757
  category_name = request.form.get('category_name')
758
  if category_name and category_name not in categories:
 
776
  description = request.form.get('description')
777
  category = request.form.get('category')
778
  photos_files = request.files.getlist('photos')
779
+ colors = [c.strip() for c in request.form.getlist('colors') if c.strip()] # Trim whitespace and remove empty
780
  photos_list = []
781
+
782
  if photos_files:
783
+ for photo in photos_files[:10]: # Limit to 10 photos
784
  if photo and photo.filename:
785
+ photo_filename = secure_filename(f"{int(time.time())}_{photo.filename}") # Add timestamp to avoid collision
786
  uploads_dir = 'uploads'
787
  os.makedirs(uploads_dir, exist_ok=True)
788
  temp_path = os.path.join(uploads_dir, photo_filename)
789
+ try:
790
+ photo.save(temp_path)
791
+ api = HfApi()
792
+ api.upload_file(
793
+ path_or_fileobj=temp_path,
794
+ path_in_repo=f"photos/{photo_filename}",
795
+ repo_id=REPO_ID,
796
+ repo_type="dataset",
797
+ token=HF_TOKEN_WRITE,
798
+ commit_message=f"Добавлено фото для товара {name}"
799
+ )
800
+ photos_list.append(photo_filename)
801
+ except Exception as e:
802
+ logging.error(f"Ошибка при загрузке фото {photo_filename}: {e}")
803
+ finally:
804
+ if os.path.exists(temp_path):
805
+ try:
806
+ os.remove(temp_path)
807
+ except OSError as e:
808
+ logging.error(f"Ошибка при удалении временного файла {temp_path}: {e}")
809
+
810
+
811
  if not name or not price or not description:
812
  return "Ошибка: Заполните все обязательные поля", 400
813
+
814
+ try:
815
+ price = float(price.replace(',', '.'))
816
+ except ValueError:
817
+ return "Ошибка: Неверный формат цены", 400
818
+
819
  new_product = {
820
  'name': name,
821
  'price': price,
822
  'description': description,
823
  'category': category if category in categories else 'Без категории',
824
  'photos': photos_list,
825
+ 'colors': colors,
826
  'added_at': datetime.now().isoformat()
827
  }
828
  products.append(new_product)
829
  save_data(data)
830
  return redirect(url_for('admin'))
831
+
832
  elif action == 'edit':
833
+ try:
834
+ index = int(request.form.get('index'))
835
+ if not 0 <= index < len(products):
836
+ return "Ошибка: Неверный индекс товара", 400
837
+ except (ValueError, TypeError):
838
+ return "Ошибка: Неверный индекс товара", 400
839
+
840
  name = request.form.get('name')
841
  price = request.form.get('price')
842
  description = request.form.get('description')
843
  category = request.form.get('category')
844
  photos_files = request.files.getlist('photos')
845
+ colors = [c.strip() for c in request.form.getlist('colors') if c.strip()] # Trim whitespace and remove empty
846
+
847
+ if not name or not price or not description:
848
+ return "Ошибка: Заполните все обязательные поля", 400
849
+
850
+ try:
851
+ price_float = float(price.replace(',', '.'))
852
+ except ValueError:
853
+ return "Ошибка: Неверный формат цены", 400
854
+
855
+
856
  if photos_files and any(photo.filename for photo in photos_files):
857
  new_photos_list = []
858
+ for photo in photos_files[:10]: # Limit to 10 photos
859
  if photo and photo.filename:
860
+ photo_filename = secure_filename(f"{int(time.time())}_{photo.filename}") # Add timestamp
861
  uploads_dir = 'uploads'
862
  os.makedirs(uploads_dir, exist_ok=True)
863
  temp_path = os.path.join(uploads_dir, photo_filename)
864
+ try:
865
+ photo.save(temp_path)
866
+ api = HfApi()
867
+ api.upload_file(
868
+ path_or_fileobj=temp_path,
869
+ path_in_repo=f"photos/{photo_filename}",
870
+ repo_id=REPO_ID,
871
+ repo_type="dataset",
872
+ token=HF_TOKEN_WRITE,
873
+ commit_message=f"Обновлено фото для товара {name}"
874
+ )
875
+ new_photos_list.append(photo_filename)
876
+ except Exception as e:
877
+ logging.error(f"Ошибка при загрузке фото {photo_filename} при редактировании: {e}")
878
+ finally:
879
+ if os.path.exists(temp_path):
880
+ try:
881
+ os.remove(temp_path)
882
+ except OSError as e:
883
+ logging.error(f"Ошибка при удалении временного файла {temp_path} при редактировании: {e}")
884
+
885
+ # Optionally delete old photos from HF here if needed, requires more logic
886
+ products[index]['photos'] = new_photos_list # Replace photos only if new ones were uploaded
887
+
888
  products[index]['name'] = name
889
+ products[index]['price'] = price_float
890
  products[index]['description'] = description
891
  products[index]['category'] = category if category in categories else 'Без категории'
892
+ products[index]['colors'] = colors
893
+ # Keep added_at timestamp
894
  save_data(data)
895
  return redirect(url_for('admin'))
896
+
897
  elif action == 'delete':
898
+ try:
899
+ index = int(request.form.get('index'))
900
+ if not 0 <= index < len(products):
901
+ return "Ошибка: Неверный индекс товара", 400
902
+ # Optionally delete photos from HF here if needed
903
+ del products[index]
904
+ save_data(data)
905
+ except (ValueError, TypeError):
906
+ return "Ошибка: Неверный индекс товара", 400
907
+ except Exception as e:
908
+ logging.error(f"Ошибка при удалении товара: {e}")
909
+ return "Ошибка при удалении товара", 500
910
  return redirect(url_for('admin'))
911
+
912
  admin_html = '''
913
  <!DOCTYPE html>
914
  <html lang="ru">
 
917
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
918
  <title>Админ-панель</title>
919
  <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
920
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"> <!-- Added Font Awesome -->
921
  <style>
922
+ :root {
923
+ --primary-color: #FFA726; /* Orange/Amber */
924
+ --secondary-color: #FFCA28; /* Lighter Orange/Amber */
925
+ --text-color: #333; /* Dark Gray */
926
+ --bg-color: #FFF8E1; /* Very Light Yellow/Amber */
927
+ --light-text: #fff; /* White */
928
  --shadow-color: rgba(0, 0, 0, 0.1);
929
+ --accent-color: #D84315; /* Deep Orange */
930
+ --hover-primary-color: #FB8C00; /* Darker Orange */
931
+ --hover-secondary-color: #FFA000; /* Darker Amber */
932
+ --delete-color: #ef4444;
933
+ --delete-hover-color: #dc2626;
934
+ --edit-bg-color: #fffaf0; /* Lighter than bg for edit form */
935
  }
936
  body {
937
  font-family: 'Poppins', sans-serif;
938
  background: var(--bg-color);
939
  color: var(--text-color);
940
  padding: 20px;
941
+ margin: 0;
942
  }
943
  .container {
944
  max-width: 1200px;
 
949
  align-items: center;
950
  padding: 15px 0;
951
  border-bottom: 1px solid var(--primary-color);
952
+ margin-bottom: 30px;
953
  }
954
  .header-logo {
955
  width: 60px;
 
966
  }
967
  h1, h2 {
968
  font-weight: 600;
969
+ margin-top: 0; /* Reset margin */
970
  margin-bottom: 20px;
971
  color: var(--primary-color);
972
  }
973
+ h1 { font-size: 1.8rem; }
974
+ h2 { font-size: 1.5rem; margin-top: 30px; } /* Add space before H2 sections */
975
+
976
  form {
977
  background: var(--light-text);
978
+ padding: 25px;
979
  border-radius: 15px;
980
  box-shadow: 0 4px 15px var(--shadow-color);
981
  margin-bottom: 30px;
 
984
  font-weight: 500;
985
  margin-top: 15px;
986
  display: block;
987
+ color: #555; /* Slightly softer label color */
988
+ font-size: 0.9rem;
989
  }
990
+ input[type="text"],
991
+ input[type="number"],
992
+ textarea,
993
+ select,
994
+ input[type="file"] {
995
  width: 100%;
996
  padding: 12px;
997
+ margin-top: 8px; /* Increased space */
998
  border: 1px solid var(--secondary-color);
999
  border-radius: 8px;
1000
  font-size: 1rem;
1001
  transition: all 0.3s ease;
1002
+ box-sizing: border-box; /* Include padding in width */
1003
  }
1004
  input:focus, textarea:focus, select:focus {
1005
  border-color: var(--primary-color);
1006
+ box-shadow: 0 0 8px rgba(255, 167, 38, 0.3); /* Adjusted focus shadow */
1007
  outline: none;
1008
  }
1009
+ textarea {
1010
+ resize: vertical; /* Allow vertical resize only */
1011
+ min-height: 80px;
1012
+ }
1013
+ input[type="file"] {
1014
+ padding: 10px; /* Adjust padding for file input */
1015
+ background-color: #fff; /* Ensure bg color */
1016
+ }
1017
+ /* Style file input button */
1018
+ input[type="file"]::file-selector-button {
1019
+ padding: 8px 15px;
1020
+ border: none;
1021
+ border-radius: 6px;
1022
+ background-color: var(--secondary-color);
1023
+ color: var(--text-color);
1024
+ font-weight: 500;
1025
+ cursor: pointer;
1026
+ transition: background-color 0.3s ease;
1027
+ margin-right: 10px;
1028
+ }
1029
+ input[type="file"]::file-selector-button:hover {
1030
+ background-color: var(--hover-secondary-color);
1031
+ color: var(--light-text);
1032
+ }
1033
+
1034
+ button[type="submit"], button[type="button"] {
1035
+ padding: 12px 25px; /* More padding */
1036
  border: none;
1037
  border-radius: 8px;
1038
  background-color: var(--primary-color);
1039
  color: var(--light-text);
1040
+ font-weight: 600; /* Bolder text */
1041
+ font-size: 1rem; /* Consistent font size */
1042
  cursor: pointer;
1043
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
1044
+ margin-top: 20px; /* More space above button */
1045
+ margin-right: 10px; /* Space between buttons if inline */
1046
  }
1047
+ button[type="submit"]:hover, button[type="button"]:hover {
1048
+ background-color: var(--hover-primary-color);
1049
+ box-shadow: 0 4px 15px rgba(251, 140, 0, 0.4);
1050
  transform: translateY(-2px);
1051
  }
1052
  .delete-button {
1053
+ background-color: var(--delete-color);
1054
  }
1055
  .delete-button:hover {
1056
+ background-color: var(--delete-hover-color);
1057
  box-shadow: 0 4px 15px rgba(220, 38, 38, 0.4);
1058
+ }
1059
+ .add-color-btn {
1060
+ background-color: var(--secondary-color);
1061
+ color: var(--text-color); /* Use text color for better contrast on light amber */
1062
+ }
1063
+ .add-color-btn:hover {
1064
+ background-color: var(--hover-secondary-color);
1065
+ color: var(--light-text); /* Switch to light text on darker hover */
1066
+ box-shadow: 0 4px 15px rgba(255, 160, 0, 0.4);
1067
  }
1068
  .product-list, .category-list {
1069
  display: grid;
1070
+ gap: 25px; /* Increased gap */
1071
  }
1072
  .product-item, .category-item {
1073
  background: var(--light-text);
1074
+ padding: 25px; /* More padding */
1075
  border-radius: 15px;
1076
  box-shadow: 0 4px 15px var(--shadow-color);
1077
+ transition: box-shadow 0.3s ease;
1078
+ }
1079
+ .product-item:hover, .category-item:hover {
1080
+ box-shadow: 0 6px 20px rgba(0,0,0,0.15); /* Subtle hover shadow */
1081
+ }
1082
+ .category-item {
1083
+ display: flex;
1084
+ justify-content: space-between;
1085
+ align-items: center;
1086
+ }
1087
+ .category-item h3 {
1088
+ margin: 0; /* Remove margin from h3 inside item */
1089
+ color: var(--text-color);
1090
+ font-weight: 500;
1091
+ }
1092
+ .category-item form {
1093
+ background: none;
1094
+ padding: 0;
1095
+ box-shadow: none;
1096
+ margin: 0;
1097
+ }
1098
+ .category-item button {
1099
+ margin: 0; /* Remove margin from button inside item */
1100
+ }
1101
+
1102
+ .product-item h3 {
1103
+ margin-top: 0;
1104
+ margin-bottom: 15px;
1105
+ color: var(--accent-color); /* Use accent for product titles */
1106
+ }
1107
+ .product-item p {
1108
+ margin-bottom: 8px;
1109
+ font-size: 0.9rem;
1110
+ line-height: 1.5;
1111
+ }
1112
+ .product-item p strong {
1113
+ color: #555;
1114
+ }
1115
+ .product-item .product-photos {
1116
+ display: flex;
1117
+ flex-wrap: wrap;
1118
+ gap: 10px;
1119
+ margin-top: 15px;
1120
+ margin-bottom: 15px;
1121
+ }
1122
+ .product-item .product-photos img {
1123
+ max-width: 100px;
1124
+ height: 100px; /* Fixed height */
1125
+ object-fit: cover; /* Cover ensures image fills space */
1126
+ border-radius: 10px;
1127
+ border: 1px solid #eee; /* Light border */
1128
+ }
1129
+ .product-item details {
1130
+ margin-top: 20px;
1131
+ }
1132
+ .product-item summary {
1133
+ cursor: pointer;
1134
+ font-weight: 500;
1135
+ color: var(--primary-color);
1136
+ padding: 8px 0; /* Add padding for easier clicking */
1137
+ outline: none; /* Remove default outline */
1138
+ display: inline-block; /* Prevent full width */
1139
+ }
1140
+ .product-item summary:hover {
1141
+ color: var(--hover-primary-color);
1142
+ }
1143
  .edit-form {
1144
  margin-top: 15px;
1145
+ padding: 20px;
1146
+ background: var(--edit-bg-color);
1147
  border-radius: 10px;
1148
+ border: 1px solid #eee; /* Light border for separation */
1149
+ }
1150
+ .color-input-container { /* Renamed for clarity */
1151
+ margin-top: 10px;
1152
+ margin-bottom: 10px;
1153
  }
1154
  .color-input-group {
1155
  display: flex;
1156
  gap: 10px;
1157
+ margin-bottom: 10px; /* Space between color inputs */
1158
+ align-items: center; /* Align input and remove button */
1159
+ }
1160
+ .color-input-group input {
1161
+ margin-top: 0; /* Reset margin from general input style */
1162
+ flex-grow: 1; /* Allow input to take available space */
1163
+ }
1164
+ .color-input-group .remove-color-btn {
1165
+ padding: 5px 10px;
1166
+ font-size: 0.8rem;
1167
+ background-color: #ddd;
1168
+ color: #333;
1169
+ border-radius: 50%;
1170
+ margin-top: 0; /* Reset margin */
1171
+ min-width: 25px; /* Ensure circle */
1172
+ height: 25px;
1173
+ line-height: 1;
1174
+ }
1175
+ .color-input-group .remove-color-btn:hover {
1176
+ background-color: #ccc;
1177
+ transform: none; /* No hover transform */
1178
+ box-shadow: none;
1179
+ }
1180
+
1181
+ .button-group { /* Group buttons like Edit/Delete */
1182
+ margin-top: 20px;
1183
+ display: flex;
1184
+ gap: 10px;
1185
+ }
1186
+ .button-group form { /* Reset form styles within button group */
1187
+ background: none;
1188
+ padding: 0;
1189
+ margin: 0;
1190
+ box-shadow: none;
1191
+ }
1192
+ .button-group button {
1193
+ margin-top: 0; /* Reset margin for buttons in group */
1194
+ }
1195
+
1196
+ /* Responsive Adjustments */
1197
+ @media (max-width: 768px) {
1198
+ .product-list, .category-list {
1199
+ grid-template-columns: 1fr; /* Stack items on smaller screens */
1200
+ }
1201
+ .header {
1202
+ flex-direction: column;
1203
+ align-items: flex-start; /* Align left */
1204
+ }
1205
+ .header h1 {
1206
+ margin-top: 10px;
1207
+ }
1208
+ form {
1209
+ padding: 20px;
1210
+ }
1211
+ }
1212
+
1213
  </style>
1214
  </head>
1215
  <body>
 
1218
  <img src="''' + LOGO_URL + '''" alt="Logo" class="header-logo">
1219
  <h1>Админ-панель</h1>
1220
  </div>
1221
+
1222
+ <h2>Добавить новый товар</h2>
1223
  <form method="POST" enctype="multipart/form-data">
1224
  <input type="hidden" name="action" value="add">
1225
+ <label for="add-name">Название товара:</label>
1226
+ <input type="text" id="add-name" name="name" required>
1227
+
1228
+ <label for="add-price">Цена (с):</label>
1229
+ <input type="number" id="add-price" name="price" step="0.01" required placeholder="Например: 150.50">
1230
+
1231
+ <label for="add-description">Описание:</label>
1232
+ <textarea id="add-description" name="description" rows="4" required></textarea>
1233
+
1234
+ <label for="add-category">Категория:</label>
1235
+ <select id="add-category" name="category">
1236
  <option value="Без категории">Без категории</option>
1237
  {% for category in categories %}
1238
  <option value="{{ category }}">{{ category }}</option>
1239
  {% endfor %}
1240
  </select>
1241
+
1242
+ <label for="add-photos">Фотографии (до 10):</label>
1243
+ <input type="file" id="add-photos" name="photos" accept="image/*" multiple>
1244
+
1245
+ <label>Доступные цвета:</label>
1246
+ <div id="add-color-inputs" class="color-input-container">
1247
  <div class="color-input-group">
1248
  <input type="text" name="colors" placeholder="Например: Красный">
1249
+ <button type="button" class="remove-color-btn" onclick="removeColorInput(this)" style="display: none;">X</button> <!-- Hide first remove btn -->
1250
  </div>
1251
  </div>
1252
+ <button type="button" class="add-color-btn" onclick="addColorInput('add-color-inputs')"><i class="fas fa-plus"></i> Добавить цвет</button>
1253
+
1254
+ <button type="submit"><i class="fas fa-plus-circle"></i> Добавить товар</button>
1255
  </form>
1256
 
1257
+ <h2>Управление категориями</h2>
1258
  <form method="POST">
1259
  <input type="hidden" name="action" value="add_category">
1260
+ <label for="category-name">Название новой категории:</label>
1261
+ <input type="text" id="category-name" name="category_name" required>
1262
+ <button type="submit"><i class="fas fa-plus"></i> Добавить категорию</button>
1263
  </form>
1264
 
1265
+ <h3>Список категорий</h3>
1266
  <div class="category-list">
1267
  {% for category in categories %}
1268
  <div class="category-item">
 
1270
  <form method="POST" style="display: inline;">
1271
  <input type="hidden" name="action" value="delete_category">
1272
  <input type="hidden" name="category_index" value="{{ loop.index0 }}">
1273
+ <button type="submit" class="delete-button"><i class="fas fa-trash-alt"></i> Удалить</button>
1274
  </form>
1275
  </div>
1276
+ {% else %}
1277
+ <p>Нет добавленных категорий.</p>
1278
  {% endfor %}
1279
  </div>
1280
 
1281
  <h2>Управление базой данных</h2>
1282
+ <div class="button-group"> <!-- Group DB buttons -->
1283
+ <form method="POST" action="{{ url_for('backup') }}">
1284
+ <button type="submit"><i class="fas fa-cloud-upload-alt"></i> Создать резервную копию</button>
1285
+ </form>
1286
+ <form method="GET" action="{{ url_for('download') }}">
1287
+ <button type="submit" class="add-color-btn"><i class="fas fa-cloud-download-alt"></i> Скачать актуальную базу</button> <!-- Style download differently -->
1288
+ </form>
1289
+ </div>
1290
 
1291
  <h2>Список товаров</h2>
1292
  <div class="product-list">
 
1296
  <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
1297
  <p><strong>Цена:</strong> {{ product['price'] }} с</p>
1298
  <p><strong>Описание:</strong> {{ product['description'] }}</p>
1299
+ <p><strong>Цвета:</strong> {{ product.get('colors', [])|join(', ') if product.get('colors') else 'Нет цветов' }}</p>
1300
  {% if product.get('photos') and product['photos']|length > 0 %}
1301
+ <div class="product-photos">
1302
  {% for photo in product['photos'] %}
1303
+ <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo }}?download=true" <!-- Added download=true for potential direct link -->
1304
+ alt="Фото {{ product['name'] }}"
1305
+ loading="lazy">
1306
  {% endfor %}
1307
  </div>
1308
  {% endif %}
1309
+ <div class="button-group">
1310
+ <details style="margin-top: 0;"> <!-- Remove top margin for details in button group -->
1311
+ <summary><i class="fas fa-edit"></i> Редактировать</summary>
1312
+ <form method="POST" enctype="multipart/form-data" class="edit-form">
1313
+ <input type="hidden" name="action" value="edit">
1314
+ <input type="hidden" name="index" value="{{ loop.index0 }}">
1315
+ <label for="edit-name-{{ loop.index0 }}">Название:</label>
1316
+ <input type="text" id="edit-name-{{ loop.index0 }}" name="name" value="{{ product['name'] }}" required>
1317
+ <label for="edit-price-{{ loop.index0 }}">Цена (с):</label>
1318
+ <input type="number" id="edit-price-{{ loop.index0 }}" name="price" step="0.01" value="{{ product['price'] }}" required>
1319
+ <label for="edit-description-{{ loop.index0 }}">Описание:</label>
1320
+ <textarea id="edit-description-{{ loop.index0 }}" name="description" rows="4" required>{{ product['description'] }}</textarea>
1321
+ <label for="edit-category-{{ loop.index0 }}">Категория:</label>
1322
+ <select id="edit-category-{{ loop.index0 }}" name="category">
1323
+ <option value="Без категории" {% if product.get('category', 'Без категории') == 'Без категории' %}selected{% endif %}>Без категории</option>
1324
+ {% for category in categories %}
1325
+ <option value="{{ category }}" {% if product.get('category') == category %}selected{% endif %}>{{ category }}</option>
1326
+ {% endfor %}
1327
+ </select>
1328
+ <label for="edit-photos-{{ loop.index0 }}">Заменить фотографии (до 10):</label>
1329
+ <input type="file" id="edit-photos-{{ loop.index0 }}" name="photos" accept="image/*" multiple>
1330
+ <label>Доступные цвета:</label>
1331
+ <div id="edit-color-inputs-{{ loop.index0 }}" class="color-input-container">
1332
+ {% set colors = product.get('colors', []) %}
1333
+ {% if colors %}
1334
+ {% for color in colors %}
1335
+ <div class="color-input-group">
1336
+ <input type="text" name="colors" value="{{ color }}">
1337
+ <button type="button" class="remove-color-btn" onclick="removeColorInput(this)">X</button>
1338
+ </div>
1339
+ {% endfor %}
1340
+ {% else %}
1341
+ <div class="color-input-group"> <!-- Add one empty if none exist -->
1342
+ <input type="text" name="colors" placeholder="Например: Синий">
1343
+ <button type="button" class="remove-color-btn" onclick="removeColorInput(this)" style="display: none;">X</button>
1344
+ </div>
1345
+ {% endif %}
1346
  </div>
1347
+ <button type="button" class="add-color-btn" onclick="addColorInput('edit-color-inputs-{{ loop.index0 }}')"><i class="fas fa-plus"></i> Добавить цвет</button>
1348
+ <button type="submit"><i class="fas fa-save"></i> Сохранить изменения</button>
1349
+ </form>
1350
+ </details>
1351
+ <form method="POST">
1352
+ <input type="hidden" name="action" value="delete">
1353
+ <input type="hidden" name="index" value="{{ loop.index0 }}">
1354
+ <button type="submit" class="delete-button" onclick="return confirm('Вы уверены, что хотите удалить этот товар?');"><i class="fas fa-trash-alt"></i> Удалить товар</button>
1355
  </form>
1356
+ </div>
 
 
 
 
 
1357
  </div>
1358
+ {% else %}
1359
+ <p>В базе пока нет товаров.</p>
1360
  {% endfor %}
1361
  </div>
1362
  </div>
1363
  <script>
1364
+ function addColorInput(containerId) {
1365
  const container = document.getElementById(containerId);
1366
+ const newInputGroup = document.createElement('div');
1367
+ newInputGroup.className = 'color-input-group';
1368
+ newInputGroup.innerHTML = `
1369
+ <input type="text" name="colors" placeholder="Еще один цвет">
1370
+ <button type="button" class="remove-color-btn" onclick="removeColorInput(this)">X</button>
1371
+ `;
1372
+ // Ensure all remove buttons are visible now
1373
+ container.querySelectorAll('.remove-color-btn').forEach(btn => btn.style.display = 'inline-block');
1374
+ container.appendChild(newInputGroup);
1375
+ }
1376
+
1377
+ function removeColorInput(button) {
1378
+ const container = button.closest('.color-input-container');
1379
+ button.closest('.color-input-group').remove();
1380
+ // If only one input remains, hide its remove button
1381
+ const remainingGroups = container.querySelectorAll('.color-input-group');
1382
+ if (remainingGroups.length === 1) {
1383
+ const lastRemoveBtn = remainingGroups[0].querySelector('.remove-color-btn');
1384
+ if(lastRemoveBtn) lastRemoveBtn.style.display = 'none';
1385
+ }
1386
+ // If no inputs remain after deletion (edge case), add a new empty one
1387
+ if (remainingGroups.length === 0) {
1388
+ addColorInput(container.id);
1389
+ }
1390
  }
1391
+
1392
+ // Initialize: Hide remove button if only one color input exists initially
1393
+ document.querySelectorAll('.color-input-container').forEach(container => {
1394
+ const groups = container.querySelectorAll('.color-input-group');
1395
+ if (groups.length === 1) {
1396
+ const removeBtn = groups[0].querySelector('.remove-color-btn');
1397
+ if (removeBtn) removeBtn.style.display = 'none';
1398
+ }
1399
+ });
1400
  </script>
1401
  </body>
1402
  </html>
 
1405
 
1406
  @app.route('/backup', methods=['POST'])
1407
  def backup():
1408
+ try:
1409
+ upload_db_to_hf()
1410
+ return "Резервная копия успешно создана и загружена на Hugging Face.", 200
1411
+ except Exception as e:
1412
+ logging.error(f"Ошибка при ручном создании резервной копии: {e}")
1413
+ return f"Ошибка при создании резервной копии: {e}", 500
1414
+
1415
 
1416
  @app.route('/download', methods=['GET'])
1417
  def download():
1418
+ try:
1419
+ download_db_from_hf()
1420
+ # Optional: Send the file to the user if needed, but current logic just updates local file
1421
+ # from flask import send_file
1422
+ # return send_file(DATA_FILE, as_attachment=True)
1423
+ return "Актуальная база данных успешно скачана из Hugging Face.", 200
1424
+ except RepositoryNotFoundError:
1425
+ return "Ошибка: Репозиторий Hugging Face не найден.", 404
1426
+ except Exception as e:
1427
+ logging.error(f"Ошибка при ручном скачивании базы данных: {e}")
1428
+ return f"Ошибка при скачивании базы данных: {e}", 500
1429
+
1430
 
1431
  if __name__ == '__main__':
1432
+ # Ensure uploads directory exists
1433
+ os.makedirs('uploads', exist_ok=True)
1434
+
1435
+ # Start background backup thread
1436
  backup_thread = threading.Thread(target=periodic_backup, daemon=True)
1437
  backup_thread.start()
1438
+
1439
+ # Initial data load attempt
1440
  try:
1441
  load_data()
1442
  except Exception as e:
1443
+ # Error logged within load_data, app can still start with empty data
1444
+ logging.warning(f"Не удалось первоначально загрузить/скачать базу данных: {e}. Приложение запустится с пустыми данными.")
1445
+
1446
+ # Run the Flask app
1447
+ # Use Gunicorn or Waitress in production instead of debug=True
1448
+ app.run(debug=False, host='0.0.0.0', port=int(os.environ.get("PORT", 7860))) # Use PORT env var if available
1449
+
1450
+ # --- END OF FILE app (16) (10).py ---