Kgshop commited on
Commit
5bae6d1
·
verified ·
1 Parent(s): db16dda

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +128 -8
app.py CHANGED
@@ -684,7 +684,7 @@ ADMIN_TEMPLATE = '''
684
  <title>Админ-панель</title>
685
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
686
  <style>
687
- :root { --primary: #2d3436; --bg: #f4f6f9; --surface: #ffffff; --border: #e0e6ed; --danger: #ff7675; --success: #00b894; --info: #0984e3; }
688
  * { box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
689
  body { background: var(--bg); padding: max(20px, env(safe-area-inset-top)) 15px calc(20px + env(safe-area-inset-bottom)); margin: 0; color: #2d3436; }
690
  .container { max-width: 1000px; margin: 0 auto; }
@@ -696,6 +696,7 @@ ADMIN_TEMPLATE = '''
696
  .btn-primary { background: var(--info); }
697
  .btn-success { background: var(--success); }
698
  .btn-danger { background: var(--danger); padding: 8px 15px; font-size: 0.85rem; }
 
699
  .btn-dark { background: var(--primary); }
700
 
701
  .sync-panel { display: flex; gap: 10px; margin-bottom: 25px; flex-wrap: wrap; }
@@ -713,6 +714,10 @@ ADMIN_TEMPLATE = '''
713
  .add-cat-form input { flex: 1; min-width: 200px; }
714
  .add-cat-form button { white-space: nowrap; }
715
 
 
 
 
 
716
  .category-block { border: 1px solid var(--border); margin-bottom: 15px; border-radius: 12px; overflow: hidden; background: #fff; }
717
  .category-header { background: #fafafa; padding: 15px 20px; font-weight: 700; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border); cursor: pointer; transition: background 0.2s; }
718
  .category-header:hover { background: #f0f0f0; }
@@ -727,6 +732,7 @@ ADMIN_TEMPLATE = '''
727
  .product-name { font-weight: 600; font-size: 0.95rem; }
728
  .product-desc { font-size: 0.85rem; color: #636e72; margin-top: 2px; }
729
  .product-meta { font-size: 0.8rem; color: #b2bec3; margin-top: 4px; }
 
730
 
731
  .add-product-wrapper { display: none; }
732
  .add-product-wrapper.active { display: block; }
@@ -744,7 +750,7 @@ ADMIN_TEMPLATE = '''
744
  .header-panel { flex-direction: column; align-items: stretch; text-align: center; }
745
  .product-item { flex-direction: column; align-items: stretch; }
746
  .product-info { width: 100%; }
747
- .product-item form { align-self: flex-end; }
748
  .form-row { flex-direction: column; }
749
  }
750
  </style>
@@ -774,12 +780,17 @@ ADMIN_TEMPLATE = '''
774
  </form>
775
  </div>
776
 
 
 
 
 
 
777
  {% for category in categories %}
778
  <div class="category-block">
779
  <div class="category-header" onclick="toggleCategory('cat-{{ loop.index }}')">
780
  <div style="display: flex; align-items: center; gap: 10px;">
781
  <i class="fas fa-chevron-down" id="icon-cat-{{ loop.index }}" style="color: #636e72;"></i>
782
- <span><i class="fas fa-folder-open" style="color:var(--info); margin-right:5px;"></i> {{ category }}</span>
783
  </div>
784
  <form method="POST" style="margin:0;" onclick="event.stopPropagation();" onsubmit="return confirm('Удалить категорию и все ее товары?');">
785
  <input type="hidden" name="action" value="delete_category">
@@ -828,11 +839,32 @@ ADMIN_TEMPLATE = '''
828
  <span class="product-meta">{{ product.price }} {{ currency_code }} • Фото: {{ product.photos|length if product.photos else 0 }}/10</span>
829
  </div>
830
  </div>
831
- <form method="POST" style="margin:0;" onsubmit="return confirm('Удалить товар?');">
832
- <input type="hidden" name="action" value="delete_product">
833
- <input type="hidden" name="product_id" value="{{ product.product_id }}">
834
- <button type="submit" class="btn btn-danger"><i class="fas fa-times"></i> У��алить</button>
835
- </form>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
836
  </div>
837
  {% endif %}
838
  {% endfor %}
@@ -866,6 +898,48 @@ ADMIN_TEMPLATE = '''
866
  const form = document.getElementById(id);
867
  form.classList.toggle('active');
868
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
869
  </script>
870
  </body>
871
  </html>
@@ -1003,6 +1077,52 @@ def admin():
1003
  data['products'] = products
1004
  save_data(data)
1005
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1006
  elif action == 'delete_product':
1007
  pid = request.form.get('product_id')
1008
  data['products'] = [p for p in products if p.get('product_id') != pid]
 
684
  <title>Админ-панель</title>
685
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
686
  <style>
687
+ :root { --primary: #2d3436; --bg: #f4f6f9; --surface: #ffffff; --border: #e0e6ed; --danger: #ff7675; --success: #00b894; --info: #0984e3; --warning: #f39c12; }
688
  * { box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
689
  body { background: var(--bg); padding: max(20px, env(safe-area-inset-top)) 15px calc(20px + env(safe-area-inset-bottom)); margin: 0; color: #2d3436; }
690
  .container { max-width: 1000px; margin: 0 auto; }
 
696
  .btn-primary { background: var(--info); }
697
  .btn-success { background: var(--success); }
698
  .btn-danger { background: var(--danger); padding: 8px 15px; font-size: 0.85rem; }
699
+ .btn-warning { background: var(--warning); padding: 8px 15px; font-size: 0.85rem; }
700
  .btn-dark { background: var(--primary); }
701
 
702
  .sync-panel { display: flex; gap: 10px; margin-bottom: 25px; flex-wrap: wrap; }
 
714
  .add-cat-form input { flex: 1; min-width: 200px; }
715
  .add-cat-form button { white-space: nowrap; }
716
 
717
+ .search-bar-admin { position: relative; margin-bottom: 20px; }
718
+ .search-bar-admin i { position: absolute; left: 15px; top: 50%; transform: translateY(-50%); color: #636e72; }
719
+ .search-bar-admin input { padding-left: 40px; background: var(--surface); border: none; box-shadow: 0 4px 15px rgba(0,0,0,0.03); }
720
+
721
  .category-block { border: 1px solid var(--border); margin-bottom: 15px; border-radius: 12px; overflow: hidden; background: #fff; }
722
  .category-header { background: #fafafa; padding: 15px 20px; font-weight: 700; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border); cursor: pointer; transition: background 0.2s; }
723
  .category-header:hover { background: #f0f0f0; }
 
732
  .product-name { font-weight: 600; font-size: 0.95rem; }
733
  .product-desc { font-size: 0.85rem; color: #636e72; margin-top: 2px; }
734
  .product-meta { font-size: 0.8rem; color: #b2bec3; margin-top: 4px; }
735
+ .product-actions { display: flex; gap: 5px; }
736
 
737
  .add-product-wrapper { display: none; }
738
  .add-product-wrapper.active { display: block; }
 
750
  .header-panel { flex-direction: column; align-items: stretch; text-align: center; }
751
  .product-item { flex-direction: column; align-items: stretch; }
752
  .product-info { width: 100%; }
753
+ .product-actions { align-self: flex-end; }
754
  .form-row { flex-direction: column; }
755
  }
756
  </style>
 
780
  </form>
781
  </div>
782
 
783
+ <div class="search-bar-admin">
784
+ <i class="fas fa-search"></i>
785
+ <input type="text" id="adminSearch" placeholder="Поиск по категориям и товарам..." oninput="filterAdmin()">
786
+ </div>
787
+
788
  {% for category in categories %}
789
  <div class="category-block">
790
  <div class="category-header" onclick="toggleCategory('cat-{{ loop.index }}')">
791
  <div style="display: flex; align-items: center; gap: 10px;">
792
  <i class="fas fa-chevron-down" id="icon-cat-{{ loop.index }}" style="color: #636e72;"></i>
793
+ <span class="cat-title-text"><i class="fas fa-folder-open" style="color:var(--info); margin-right:5px;"></i> {{ category }}</span>
794
  </div>
795
  <form method="POST" style="margin:0;" onclick="event.stopPropagation();" onsubmit="return confirm('Удалить категорию и все ее товары?');">
796
  <input type="hidden" name="action" value="delete_category">
 
839
  <span class="product-meta">{{ product.price }} {{ currency_code }} • Фото: {{ product.photos|length if product.photos else 0 }}/10</span>
840
  </div>
841
  </div>
842
+ <div class="product-actions">
843
+ <button class="btn btn-warning" onclick="toggleEditProduct('edit-prod-{{ product.product_id }}')"><i class="fas fa-edit"></i></button>
844
+ <form method="POST" style="margin:0;" onsubmit="return confirm('Удалить товар?');">
845
+ <input type="hidden" name="action" value="delete_product">
846
+ <input type="hidden" name="product_id" value="{{ product.product_id }}">
847
+ <button type="submit" class="btn btn-danger"><i class="fas fa-times"></i></button>
848
+ </form>
849
+ </div>
850
+ <div class="add-product-wrapper" id="edit-prod-{{ product.product_id }}" style="width: 100%; margin-top: 15px; border-top: 1px dashed var(--border); padding-top: 15px;">
851
+ <form class="add-product-form" method="POST" enctype="multipart/form-data" onsubmit="showLoading(this)" style="padding: 0;">
852
+ <input type="hidden" name="action" value="edit_product">
853
+ <input type="hidden" name="product_id" value="{{ product.product_id }}">
854
+ <input type="hidden" name="category" value="{{ category }}">
855
+ <div style="font-weight: 600; font-size: 0.9rem; color: #636e72;">Редактирование товара</div>
856
+ <div class="form-row">
857
+ <input type="text" name="name" value="{{ product.name }}" required autocomplete="off" style="flex:2;">
858
+ <input type="number" name="price" value="{{ product.price }}" required step="0.01" style="flex:1;">
859
+ </div>
860
+ <textarea name="description">{{ product.description }}</textarea>
861
+ <div class="file-input-wrapper">
862
+ <input type="file" name="photos" accept="image/*" multiple max="10">
863
+ <div style="font-size: 0.8rem; color: #999; margin-top: 5px;">Оставьте пустым, чтобы не менять фото</div>
864
+ </div>
865
+ <button type="submit" class="btn btn-primary" style="width: 100%; justify-content: center;"><i class="fas fa-save"></i> Сохранить изменения</button>
866
+ </form>
867
+ </div>
868
  </div>
869
  {% endif %}
870
  {% endfor %}
 
898
  const form = document.getElementById(id);
899
  form.classList.toggle('active');
900
  }
901
+
902
+ function toggleEditProduct(id) {
903
+ const form = document.getElementById(id);
904
+ form.classList.toggle('active');
905
+ }
906
+
907
+ function filterAdmin() {
908
+ const query = document.getElementById('adminSearch').value.toLowerCase();
909
+ const categories = document.querySelectorAll('.category-block');
910
+
911
+ categories.forEach(cat => {
912
+ const catName = cat.querySelector('.cat-title-text').innerText.toLowerCase();
913
+ const products = cat.querySelectorAll('.product-item');
914
+ let catMatch = catName.includes(query);
915
+ let hasVisibleProduct = false;
916
+
917
+ products.forEach(prod => {
918
+ const prodName = prod.querySelector('.product-name').innerText.toLowerCase();
919
+ if (prodName.includes(query) || catMatch) {
920
+ prod.style.display = 'flex';
921
+ hasVisibleProduct = true;
922
+ } else {
923
+ prod.style.display = 'none';
924
+ }
925
+ });
926
+
927
+ if (catMatch || hasVisibleProduct) {
928
+ cat.style.display = 'block';
929
+ if (query && hasVisibleProduct) {
930
+ cat.querySelector('.category-content').classList.add('active');
931
+ cat.querySelector('.fas.fa-chevron-down, .fas.fa-chevron-up').className = 'fas fa-chevron-up';
932
+ }
933
+ } else {
934
+ cat.style.display = 'none';
935
+ }
936
+
937
+ if (!query) {
938
+ cat.querySelector('.category-content').classList.remove('active');
939
+ cat.querySelector('.fas.fa-chevron-up, .fas.fa-chevron-down').className = 'fas fa-chevron-down';
940
+ }
941
+ });
942
+ }
943
  </script>
944
  </body>
945
  </html>
 
1077
  data['products'] = products
1078
  save_data(data)
1079
 
1080
+ elif action == 'edit_product':
1081
+ pid = request.form.get('product_id')
1082
+ name = request.form.get('name', '').strip()
1083
+ price = float(request.form.get('price', 0))
1084
+ description = request.form.get('description', '').strip()
1085
+ uploaded_photos = request.files.getlist('photos')[:10]
1086
+
1087
+ photos_list = []
1088
+ if uploaded_photos and uploaded_photos[0].filename and HF_TOKEN_WRITE:
1089
+ uploads_dir = 'uploads_temp'
1090
+ os.makedirs(uploads_dir, exist_ok=True)
1091
+ api = HfApi()
1092
+ for photo in uploaded_photos:
1093
+ if photo and photo.filename:
1094
+ ext = os.path.splitext(photo.filename)[1].lower()
1095
+ if ext not in ['.jpg', '.jpeg', '.png', '.webp', '.gif']:
1096
+ continue
1097
+ photo_filename = f"{uuid4().hex}{ext}"
1098
+ temp_path = os.path.join(uploads_dir, photo_filename)
1099
+ photo.save(temp_path)
1100
+ try:
1101
+ api.upload_file(
1102
+ path_or_fileobj=temp_path,
1103
+ path_in_repo=f"photos/{photo_filename}",
1104
+ repo_id=REPO_ID,
1105
+ repo_type="dataset",
1106
+ token=HF_TOKEN_WRITE
1107
+ )
1108
+ photos_list.append(photo_filename)
1109
+ except Exception:
1110
+ pass
1111
+ finally:
1112
+ if os.path.exists(temp_path):
1113
+ os.remove(temp_path)
1114
+
1115
+ for p in products:
1116
+ if p.get('product_id') == pid:
1117
+ p['name'] = name
1118
+ p['price'] = price
1119
+ p['description'] = description
1120
+ if photos_list:
1121
+ p['photos'] = photos_list
1122
+ break
1123
+ data['products'] = products
1124
+ save_data(data)
1125
+
1126
  elif action == 'delete_product':
1127
  pid = request.form.get('product_id')
1128
  data['products'] = [p for p in products if p.get('product_id') != pid]