Update app.py
Browse files
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-
|
| 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 |
-
<
|
| 832 |
-
<
|
| 833 |
-
<
|
| 834 |
-
|
| 835 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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]
|