Update app.py
Browse files
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': []
|
|
|
|
|
|
|
| 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
|
| 98 |
data = load_data()
|
| 99 |
products = data['products']
|
| 100 |
categories = data['categories']
|
|
|
|
| 101 |
|
| 102 |
-
|
| 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 -
|
| 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
|
| 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
|
| 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="
|
| 448 |
</div>
|
| 449 |
{% endfor %}
|
| 450 |
</div>
|
|
@@ -458,13 +464,13 @@ def catalog():
|
|
| 458 |
</div>
|
| 459 |
</div>
|
| 460 |
|
| 461 |
-
<!--
|
| 462 |
-
<div id="
|
| 463 |
<div class="modal-content">
|
| 464 |
-
<span class="close" onclick="closeModal('
|
| 465 |
-
<h2
|
| 466 |
<input type="number" id="quantityInput" class="quantity-input" min="1" value="1">
|
| 467 |
-
<
|
| 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
|
| 540 |
selectedProductIndex = index;
|
| 541 |
const product = products[index];
|
| 542 |
-
const
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 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
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
}
|
| 586 |
|
| 587 |
localStorage.setItem('cart', JSON.stringify(cart));
|
| 588 |
-
closeModal('
|
| 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
|
|
|
|
| 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.
|
|
|
|
|
|
|
| 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
|
|
|
|
| 633 |
total += itemTotal;
|
| 634 |
-
orderText += `${index + 1}. ${item.name} - ${item.
|
|
|
|
|
|
|
| 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(
|
| 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 "
|
| 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]:
|
| 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"Добавлено фото для
|
| 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]:
|
| 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"Обновлено фото для
|
| 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>Добавление
|
| 965 |
<form method="POST" enctype="multipart/form-data">
|
| 966 |
<input type="hidden" name="action" value="add">
|
| 967 |
-
<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 |
-
<
|
| 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>Список
|
| 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():
|