Update app.py
Browse files
app.py
CHANGED
|
@@ -173,13 +173,24 @@ CATEGORY_PAGE_TEMPLATE = '''
|
|
| 173 |
<title>Каталог</title>
|
| 174 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
| 175 |
<style>
|
| 176 |
-
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 0; background-color: #f0f2f5; color: #1c1e21; }
|
| 177 |
-
.header { background-color: #fff; padding: 10px 16px; border-bottom: 1px solid #dddfe2; text-align: center; font-size: 1.5rem; font-weight: 600; }
|
| 178 |
.container { padding: 8px; }
|
|
|
|
|
|
|
| 179 |
.category-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
| 180 |
-
.category-card { display: flex; justify-content:
|
| 181 |
-
.category-card:active { background-color: #
|
| 182 |
-
.product-count { background-color: #e4e6eb; color: #606770; font-size: 0.8rem; padding:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
</style>
|
| 184 |
</head>
|
| 185 |
<body>
|
|
@@ -187,6 +198,11 @@ CATEGORY_PAGE_TEMPLATE = '''
|
|
| 187 |
Категории
|
| 188 |
</div>
|
| 189 |
<div class="container">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
<div class="category-grid">
|
| 191 |
{% for category, count in categories_with_counts.items() %}
|
| 192 |
<a href="{{ url_for('product_list', category_name=category) }}" class="category-card">
|
|
@@ -196,6 +212,88 @@ CATEGORY_PAGE_TEMPLATE = '''
|
|
| 196 |
{% endfor %}
|
| 197 |
</div>
|
| 198 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
</body>
|
| 200 |
</html>
|
| 201 |
'''
|
|
@@ -206,44 +304,60 @@ PRODUCT_PAGE_TEMPLATE = '''
|
|
| 206 |
<head>
|
| 207 |
<meta charset="UTF-8">
|
| 208 |
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
| 209 |
-
<title>{{
|
| 210 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
| 211 |
<style>
|
| 212 |
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 0; background-color: #f0f2f5; color: #1c1e21; padding-bottom: 80px; }
|
| 213 |
-
.header { display: flex; align-items: center; background-color: #fff; padding: 10px 16px; border-bottom: 1px solid #dddfe2; }
|
| 214 |
.header a { color: #007bff; text-decoration: none; font-size: 1.2rem; }
|
| 215 |
.header h1 { font-size: 1.2rem; font-weight: 600; margin: 0; position: absolute; left: 50%; transform: translateX(-50%); }
|
|
|
|
|
|
|
| 216 |
.product-list { display: flex; flex-direction: column; gap: 8px; padding: 8px; }
|
| 217 |
-
.product-card { display: flex; align-items: center; padding: 12px; background-color: #fff; border: 1px solid #dddfe2; border-radius:
|
| 218 |
-
.product-card img { width: 80px; height: 80px; object-fit: cover; border-radius:
|
| 219 |
.product-info { flex-grow: 1; }
|
| 220 |
.product-name { font-size: 1rem; font-weight: 500; margin: 0 0 4px 0; }
|
| 221 |
.product-price { font-size: 0.9rem; color: #606770; margin: 0; }
|
| 222 |
.quantity-selector { display: flex; align-items: center; gap: 10px; }
|
| 223 |
-
.quantity-btn { width: 32px; height: 32px; border: 1px solid #ccc; background-color: #
|
|
|
|
| 224 |
.quantity-input { width: 40px; text-align: center; border: 1px solid #ccc; border-radius: 4px; padding: 5px; font-size: 1rem; -moz-appearance: textfield; }
|
| 225 |
.quantity-input::-webkit-outer-spin-button, .quantity-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
|
| 226 |
.floating-cart-button { position: fixed; bottom: 20px; right: 20px; background-color: #007bff; color: white; width: 60px; height: 60px; border-radius: 50%; border: none; display: flex; align-items: center; justify-content: center; font-size: 1.5rem; box-shadow: 0 4px 12px rgba(0,0,0,0.15); cursor: pointer; z-index: 1000; }
|
| 227 |
.cart-count { position: absolute; top: 0; right: 0; background-color: #dc3545; color: white; font-size: 0.75rem; width: 20px; height: 20px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; }
|
| 228 |
.modal { display: none; position: fixed; z-index: 1001; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); }
|
| 229 |
-
.modal-content { background-color: #fefefe; margin:
|
| 230 |
.close { color: #aaa; float: right; font-size: 28px; font-weight: bold; cursor: pointer; }
|
| 231 |
.cart-item { display: flex; justify-content: space-between; align-items: center; padding: 10px 0; border-bottom: 1px solid #eee; }
|
| 232 |
.cart-item-info { font-size: 0.9rem; }
|
| 233 |
.cart-total { text-align: right; margin-top: 15px; font-weight: bold; font-size: 1.1rem; }
|
| 234 |
.formulate-order-button { width: 100%; padding: 12px; background-color: #28a745; color: white; border: none; border-radius: 6px; font-size: 1rem; font-weight: bold; cursor: pointer; margin-top: 15px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
</style>
|
| 236 |
</head>
|
| 237 |
<body>
|
| 238 |
<div class="header">
|
| 239 |
<a href="{{ url_for('catalog') }}"><i class="fas fa-chevron-left"></i></a>
|
| 240 |
-
<h1>{{
|
|
|
|
|
|
|
|
|
|
| 241 |
</div>
|
| 242 |
<div class="product-list" id="product-list">
|
|
|
|
|
|
|
|
|
|
| 243 |
{% for product in products %}
|
| 244 |
-
<div class="product-card" data-product-id="{{ product.product_id }}">
|
| 245 |
{% if product.get('photos') and product['photos']|length > 0 %}
|
| 246 |
-
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product['photos'][0] }}" alt="{{ product.name }}">
|
| 247 |
{% else %}
|
| 248 |
<img src="https://via.placeholder.com/80x80.png?text=N/A" alt="No Image">
|
| 249 |
{% endif %}
|
|
@@ -264,7 +378,6 @@ PRODUCT_PAGE_TEMPLATE = '''
|
|
| 264 |
<i class="fas fa-shopping-cart"></i>
|
| 265 |
<span class="cart-count" id="cart-count">0</span>
|
| 266 |
</button>
|
| 267 |
-
|
| 268 |
<div id="cartModal" class="modal">
|
| 269 |
<div class="modal-content">
|
| 270 |
<span class="close" id="closeCartModal">×</span>
|
|
@@ -274,6 +387,14 @@ PRODUCT_PAGE_TEMPLATE = '''
|
|
| 274 |
<button class="formulate-order-button" id="formulate-order-btn">Сформировать заказ</button>
|
| 275 |
</div>
|
| 276 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 277 |
|
| 278 |
<script>
|
| 279 |
const allProducts = {{ products_json|safe }};
|
|
@@ -288,34 +409,22 @@ PRODUCT_PAGE_TEMPLATE = '''
|
|
| 288 |
function updateCart(productId, change) {
|
| 289 |
const product = getProductById(productId);
|
| 290 |
if (!product) return;
|
| 291 |
-
|
| 292 |
const existingItemIndex = cart.findIndex(item => item.product_id === productId);
|
| 293 |
-
let newQuantity
|
| 294 |
-
|
| 295 |
if (existingItemIndex > -1) {
|
| 296 |
newQuantity = cart[existingItemIndex].quantity + change;
|
| 297 |
} else {
|
| 298 |
-
|
| 299 |
}
|
| 300 |
-
|
| 301 |
if (newQuantity > 0) {
|
| 302 |
if (existingItemIndex > -1) {
|
| 303 |
cart[existingItemIndex].quantity = newQuantity;
|
| 304 |
} else {
|
| 305 |
-
cart.push({
|
| 306 |
-
product_id: product.product_id,
|
| 307 |
-
name: product.name,
|
| 308 |
-
price: product.price,
|
| 309 |
-
photo: product.photos && product.photos.length > 0 ? product.photos[0] : null,
|
| 310 |
-
quantity: newQuantity
|
| 311 |
-
});
|
| 312 |
-
}
|
| 313 |
-
} else {
|
| 314 |
-
if (existingItemIndex > -1) {
|
| 315 |
-
cart.splice(existingItemIndex, 1);
|
| 316 |
}
|
|
|
|
|
|
|
| 317 |
}
|
| 318 |
-
|
| 319 |
localStorage.setItem('gippoCart', JSON.stringify(cart));
|
| 320 |
updateUI();
|
| 321 |
}
|
|
@@ -329,14 +438,12 @@ PRODUCT_PAGE_TEMPLATE = '''
|
|
| 329 |
}
|
| 330 |
totalItems += item.quantity;
|
| 331 |
});
|
| 332 |
-
|
| 333 |
document.querySelectorAll('.product-card').forEach(card => {
|
| 334 |
const productId = card.dataset.productId;
|
| 335 |
if (!cart.some(item => item.product_id === productId)) {
|
| 336 |
card.querySelector('.quantity-input').value = 0;
|
| 337 |
}
|
| 338 |
});
|
| 339 |
-
|
| 340 |
const cartCountEl = document.getElementById('cart-count');
|
| 341 |
cartCountEl.textContent = totalItems;
|
| 342 |
cartCountEl.style.display = totalItems > 0 ? 'flex' : 'none';
|
|
@@ -353,22 +460,14 @@ PRODUCT_PAGE_TEMPLATE = '''
|
|
| 353 |
}
|
| 354 |
});
|
| 355 |
|
| 356 |
-
const
|
| 357 |
const cartButton = document.getElementById('cart-button');
|
| 358 |
-
const
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
}
|
| 364 |
-
closeButton.onclick = function() {
|
| 365 |
-
modal.style.display = "none";
|
| 366 |
-
}
|
| 367 |
-
window.onclick = function(event) {
|
| 368 |
-
if (event.target == modal) {
|
| 369 |
-
modal.style.display = "none";
|
| 370 |
-
}
|
| 371 |
-
}
|
| 372 |
|
| 373 |
function renderCartModal() {
|
| 374 |
const cartContent = document.getElementById('cart-content');
|
|
@@ -388,26 +487,69 @@ PRODUCT_PAGE_TEMPLATE = '''
|
|
| 388 |
}
|
| 389 |
|
| 390 |
document.getElementById('formulate-order-btn').addEventListener('click', () => {
|
| 391 |
-
if (cart.length === 0) {
|
| 392 |
-
|
| 393 |
-
return;
|
| 394 |
-
}
|
| 395 |
-
fetch('/create_order', {
|
| 396 |
-
method: 'POST',
|
| 397 |
-
headers: { 'Content-Type': 'application/json' },
|
| 398 |
-
body: JSON.stringify({ cart: cart })
|
| 399 |
-
})
|
| 400 |
.then(response => response.json())
|
| 401 |
.then(data => {
|
| 402 |
-
if (data.order_id) {
|
| 403 |
-
|
| 404 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 405 |
} else {
|
| 406 |
-
|
| 407 |
}
|
| 408 |
-
})
|
| 409 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 410 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 411 |
|
| 412 |
document.addEventListener('DOMContentLoaded', updateUI);
|
| 413 |
</script>
|
|
@@ -424,7 +566,7 @@ ORDER_PAGE_TEMPLATE = '''
|
|
| 424 |
<title>Накладная №{{ order.id }}</title>
|
| 425 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
| 426 |
<style>
|
| 427 |
-
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; margin: 0; background-color: #f0f2f5; }
|
| 428 |
.invoice-box { max-width: 800px; margin: 20px auto; padding: 20px; background: #fff; border: 1px solid #eee; box-shadow: 0 0 10px rgba(0, 0, 0, 0.15); font-size: 16px; line-height: 24px; color: #555; }
|
| 429 |
.invoice-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 20px; }
|
| 430 |
.invoice-header h1 { font-size: 2em; margin: 0; font-weight: 600; color: #333; }
|
|
@@ -437,13 +579,13 @@ ORDER_PAGE_TEMPLATE = '''
|
|
| 437 |
.invoice-table .center { text-align: center; }
|
| 438 |
.invoice-table .right { text-align: right; }
|
| 439 |
.total-row td { border-top: 2px solid #333; font-weight: bold; }
|
| 440 |
-
.floating-buttons { position: fixed; bottom:
|
| 441 |
-
.action-button { padding: 12px 20px; border: none; border-radius: 25px; color: white; font-weight: bold; cursor: pointer; transition: background-color 0.3s; font-size: 1rem; display: inline-flex; align-items: center; gap: 8px; text-decoration: none; box-shadow: 0 4px 10px rgba(0,0,0,0.2); }
|
| 442 |
.whatsapp-btn { background-color: #25D366; }
|
| 443 |
.print-btn { background-color: #007BFF; }
|
| 444 |
@media print {
|
| 445 |
-
body { background-color: #fff; margin: 0; }
|
| 446 |
-
.invoice-box { box-shadow: none; border: none; margin: 0; max-width: 100%; }
|
| 447 |
.floating-buttons { display: none; }
|
| 448 |
}
|
| 449 |
</style>
|
|
@@ -490,12 +632,10 @@ ORDER_PAGE_TEMPLATE = '''
|
|
| 490 |
</tfoot>
|
| 491 |
</table>
|
| 492 |
</div>
|
| 493 |
-
|
| 494 |
<div class="floating-buttons">
|
| 495 |
<button class="action-button print-btn" onclick="window.print()"><i class="fas fa-print"></i> Печать</button>
|
| 496 |
<button class="action-button whatsapp-btn" onclick="sendOrderViaWhatsApp()"><i class="fab fa-whatsapp"></i> WhatsApp</button>
|
| 497 |
</div>
|
| 498 |
-
|
| 499 |
<script>
|
| 500 |
function sendOrderViaWhatsApp() {
|
| 501 |
const orderId = "{{ order.id }}";
|
|
@@ -531,39 +671,49 @@ ADMIN_PAGE_TEMPLATE = '''
|
|
| 531 |
<meta charset="UTF-8">
|
| 532 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 533 |
<title>Админ-панель</title>
|
| 534 |
-
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600&display=swap" rel="stylesheet">
|
| 535 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
| 536 |
<style>
|
| 537 |
-
body { font-family:
|
| 538 |
-
.container { max-width: 900px; margin: 0 auto; background-color: #fff; padding:
|
| 539 |
-
.header { display: flex; justify-content: space-between; align-items: center; padding-bottom: 15px; margin-bottom:
|
| 540 |
-
h1 {
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
.
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
.
|
| 551 |
-
.
|
| 552 |
-
.
|
| 553 |
-
.
|
| 554 |
-
.product-info { flex-grow: 1; }
|
| 555 |
-
.message { padding: 10px 15px; border-radius: 6px; margin-bottom: 15px; }
|
| 556 |
.message.success { background-color: #d4edda; color: #155724; }
|
| 557 |
.message.error { background-color: #f8d7da; color: #721c24; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 558 |
</style>
|
| 559 |
</head>
|
| 560 |
<body>
|
| 561 |
<div class="container">
|
| 562 |
<div class="header">
|
| 563 |
<h1>Админ-панель</h1>
|
| 564 |
-
<a href="{{ url_for('catalog') }}" class="button" style="margin-top:0;">
|
| 565 |
</div>
|
| 566 |
-
|
| 567 |
{% with messages = get_flashed_messages(with_categories=true) %}
|
| 568 |
{% if messages %}
|
| 569 |
{% for category, message in messages %}
|
|
@@ -571,82 +721,165 @@ ADMIN_PAGE_TEMPLATE = '''
|
|
| 571 |
{% endfor %}
|
| 572 |
{% endif %}
|
| 573 |
{% endwith %}
|
| 574 |
-
|
| 575 |
<div class="section">
|
| 576 |
<h2>Управление категориями</h2>
|
| 577 |
-
<
|
| 578 |
-
<
|
| 579 |
-
<div
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
<label for="category_name">Название:</label>
|
| 583 |
-
<input type="text" id="category_name" name="category_name" required>
|
| 584 |
-
<button type="submit">Добавить</button>
|
| 585 |
-
</form>
|
| 586 |
-
</div>
|
| 587 |
-
</details>
|
| 588 |
</div>
|
| 589 |
-
|
| 590 |
<div class="section">
|
| 591 |
-
<h2>Товары
|
|
|
|
| 592 |
{% for category in categories %}
|
| 593 |
-
<
|
| 594 |
-
<
|
| 595 |
-
<
|
| 596 |
-
<
|
| 597 |
-
<
|
| 598 |
-
<
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
|
|
|
|
|
|
|
|
|
| 602 |
<div class="category-content">
|
| 603 |
{% set products_in_cat = products_by_category.get(category, []) %}
|
| 604 |
{% if products_in_cat %}
|
| 605 |
{% for product in products_in_cat %}
|
| 606 |
-
<div class="product-item">
|
| 607 |
-
{% if product.
|
| 608 |
-
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product['photos'][0] }}">
|
| 609 |
-
{% else %}
|
| 610 |
-
<img src="https://via.placeholder.com/60x60.png?text=N/A">
|
| 611 |
-
{% endif %}
|
| 612 |
<div class="product-info">
|
| 613 |
<strong>{{ product.name }}</strong><br>
|
| 614 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 615 |
</div>
|
| 616 |
-
<form method="POST" onsubmit="return confirm('Удалить товар?');" style="margin:0;">
|
| 617 |
-
<input type="hidden" name="action" value="delete_product">
|
| 618 |
-
<input type="hidden" name="product_id" value="{{ product.product_id }}">
|
| 619 |
-
<button type="submit" class="delete-button" style="margin:0;">Удалить</button>
|
| 620 |
-
</form>
|
| 621 |
</div>
|
| 622 |
{% endfor %}
|
| 623 |
{% else %}
|
| 624 |
<p>В этой категории нет товаров.</p>
|
| 625 |
{% endif %}
|
| 626 |
-
<details style="margin-top:15px;">
|
| 627 |
-
<summary>+ Добавить товар в "{{ category }}"</summary>
|
| 628 |
-
<div class="form-content">
|
| 629 |
-
<form method="POST" enctype="multipart/form-data">
|
| 630 |
-
<input type="hidden" name="action" value="add_product">
|
| 631 |
-
<input type="hidden" name="category" value="{{ category }}">
|
| 632 |
-
<label>Название:</label><input type="text" name="name" required>
|
| 633 |
-
<label>Цена:</label><input type="number" name="price" step="0.01" required>
|
| 634 |
-
<label>Описание:</label><textarea name="description" rows="3"></textarea>
|
| 635 |
-
<label>Фото (до 10):</label><input type="file" name="photos" multiple>
|
| 636 |
-
<button type="submit">Добавить товар</button>
|
| 637 |
-
</form>
|
| 638 |
-
</div>
|
| 639 |
-
</details>
|
| 640 |
</div>
|
| 641 |
-
</
|
| 642 |
{% endfor %}
|
| 643 |
</div>
|
| 644 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 645 |
</body>
|
| 646 |
</html>
|
| 647 |
'''
|
| 648 |
|
| 649 |
-
|
| 650 |
@app.route('/')
|
| 651 |
def catalog():
|
| 652 |
data = load_data()
|
|
@@ -660,10 +893,12 @@ def catalog():
|
|
| 660 |
else:
|
| 661 |
if cat not in categories:
|
| 662 |
categories_with_counts[cat] = 1
|
| 663 |
-
|
| 664 |
sorted_categories = dict(sorted(categories_with_counts.items()))
|
| 665 |
-
|
| 666 |
-
|
|
|
|
|
|
|
|
|
|
| 667 |
|
| 668 |
@app.route('/category/<category_name>')
|
| 669 |
def product_list(category_name):
|
|
@@ -672,12 +907,33 @@ def product_list(category_name):
|
|
| 672 |
return render_template_string(
|
| 673 |
PRODUCT_PAGE_TEMPLATE,
|
| 674 |
products=products_in_category,
|
| 675 |
-
|
| 676 |
products_json=json.dumps(products_in_category),
|
| 677 |
repo_id=REPO_ID,
|
| 678 |
currency_code=CURRENCY_CODE
|
| 679 |
)
|
| 680 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 681 |
@app.route('/create_order', methods=['POST'])
|
| 682 |
def create_order():
|
| 683 |
order_data = request.get_json()
|
|
@@ -719,11 +975,32 @@ def view_order(order_id):
|
|
| 719 |
order = data.get('orders', {}).get(order_id)
|
| 720 |
return render_template_string(ORDER_PAGE_TEMPLATE, order=order, currency_code=CURRENCY_CODE, whatsapp_number=WHATSAPP_NUMBER)
|
| 721 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 722 |
@app.route('/admin', methods=['GET', 'POST'])
|
| 723 |
def admin():
|
| 724 |
data = load_data()
|
| 725 |
if request.method == 'POST':
|
| 726 |
action = request.form.get('action')
|
|
|
|
| 727 |
if action == 'add_category':
|
| 728 |
category_name = request.form.get('category_name', '').strip()
|
| 729 |
if category_name and category_name not in data['categories']:
|
|
@@ -732,6 +1009,7 @@ def admin():
|
|
| 732 |
flash('Категория добавлена.', 'success')
|
| 733 |
else:
|
| 734 |
flash('Ошибка: Неверное имя или категория уже существует.', 'error')
|
|
|
|
| 735 |
elif action == 'delete_category':
|
| 736 |
category_to_delete = request.form.get('category_name')
|
| 737 |
if category_to_delete in data['categories']:
|
|
@@ -741,6 +1019,7 @@ def admin():
|
|
| 741 |
p['category'] = 'Без категории'
|
| 742 |
save_data(data)
|
| 743 |
flash('Категория удалена.', 'success')
|
|
|
|
| 744 |
elif action == 'add_product':
|
| 745 |
name = request.form.get('name', '').strip()
|
| 746 |
price_str = request.form.get('price', '').replace(',', '.')
|
|
@@ -753,24 +1032,7 @@ def admin():
|
|
| 753 |
flash("Неверный формат цены.", 'error')
|
| 754 |
return redirect(url_for('admin'))
|
| 755 |
|
| 756 |
-
photos_list =
|
| 757 |
-
photos_files = request.files.getlist('photos')
|
| 758 |
-
if photos_files and HF_TOKEN_WRITE:
|
| 759 |
-
api = HfApi()
|
| 760 |
-
for photo in photos_files[:10]:
|
| 761 |
-
if photo and photo.filename:
|
| 762 |
-
try:
|
| 763 |
-
safe_name = secure_filename(name.replace(' ', '_'))[:50]
|
| 764 |
-
ext = os.path.splitext(photo.filename)[1].lower()
|
| 765 |
-
photo_filename = f"{safe_name}_{uuid4().hex[:8]}{ext}"
|
| 766 |
-
api.upload_file(
|
| 767 |
-
path_or_fileobj=photo.stream, path_in_repo=f"photos/{photo_filename}",
|
| 768 |
-
repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE
|
| 769 |
-
)
|
| 770 |
-
photos_list.append(photo_filename)
|
| 771 |
-
except Exception:
|
| 772 |
-
flash(f"Ошибка загрузки фото {photo.filename}.", 'error')
|
| 773 |
-
|
| 774 |
new_product = {
|
| 775 |
'product_id': uuid4().hex, 'name': name, 'price': price,
|
| 776 |
'description': request.form.get('description', '').strip(),
|
|
@@ -780,6 +1042,33 @@ def admin():
|
|
| 780 |
save_data(data)
|
| 781 |
flash('Товар добавлен.', 'success')
|
| 782 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 783 |
elif action == 'delete_product':
|
| 784 |
product_id = request.form.get('product_id')
|
| 785 |
product_to_delete = next((p for p in data['products'] if p.get('product_id') == product_id), None)
|
|
@@ -794,6 +1083,7 @@ def admin():
|
|
| 794 |
data['products'] = [p for p in data['products'] if p.get('product_id') != product_id]
|
| 795 |
save_data(data)
|
| 796 |
flash('Товар удален.', 'success')
|
|
|
|
| 797 |
return redirect(url_for('admin'))
|
| 798 |
|
| 799 |
products_by_category = {}
|
|
@@ -808,7 +1098,8 @@ def admin():
|
|
| 808 |
categories=sorted(data.get('categories', [])),
|
| 809 |
products_by_category=products_by_category,
|
| 810 |
repo_id=REPO_ID,
|
| 811 |
-
currency_code=CURRENCY_CODE
|
|
|
|
| 812 |
)
|
| 813 |
|
| 814 |
if __name__ == '__main__':
|
|
|
|
| 173 |
<title>Каталог</title>
|
| 174 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
| 175 |
<style>
|
| 176 |
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 0; background-color: #f0f2f5; color: #1c1e21; padding-bottom: 80px; }
|
| 177 |
+
.header { background-color: #fff; padding: 10px 16px; border-bottom: 1px solid #dddfe2; text-align: center; font-size: 1.5rem; font-weight: 600; position: sticky; top: 0; z-index: 100; }
|
| 178 |
.container { padding: 8px; }
|
| 179 |
+
.search-container { padding: 8px; }
|
| 180 |
+
.search-bar { width: 100%; padding: 12px; font-size: 1rem; border-radius: 20px; border: 1px solid #ccd0d5; box-sizing: border-box; }
|
| 181 |
.category-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
| 182 |
+
.category-card { display: flex; flex-direction: column; justify-content: center; align-items: center; padding: 20px 16px; background-color: #fff; border: 1px solid #dddfe2; border-radius: 12px; text-decoration: none; color: #1c1e21; font-size: 1rem; font-weight: 500; transition: background-color 0.2s, transform 0.2s; text-align: center; }
|
| 183 |
+
.category-card:active { background-color: #e7f3ff; transform: scale(0.98); }
|
| 184 |
+
.product-count { background-color: #e4e6eb; color: #606770; font-size: 0.8rem; padding: 4px 10px; border-radius: 12px; font-weight: 600; margin-top: 8px; }
|
| 185 |
+
.floating-cart-button { position: fixed; bottom: 20px; right: 20px; background-color: #007bff; color: white; width: 60px; height: 60px; border-radius: 50%; border: none; display: flex; align-items: center; justify-content: center; font-size: 1.5rem; box-shadow: 0 4px 12px rgba(0,0,0,0.15); cursor: pointer; z-index: 1000; }
|
| 186 |
+
.cart-count { position: absolute; top: 0; right: 0; background-color: #dc3545; color: white; font-size: 0.75rem; width: 20px; height: 20px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; }
|
| 187 |
+
.modal { display: none; position: fixed; z-index: 1001; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); }
|
| 188 |
+
.modal-content { background-color: #fefefe; margin: auto; padding: 20px; border: 1px solid #888; width: 90%; max-width: 500px; border-radius: 8px; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); }
|
| 189 |
+
.close { color: #aaa; float: right; font-size: 28px; font-weight: bold; cursor: pointer; }
|
| 190 |
+
.cart-item { display: flex; justify-content: space-between; align-items: center; padding: 10px 0; border-bottom: 1px solid #eee; }
|
| 191 |
+
.cart-item-info { font-size: 0.9rem; }
|
| 192 |
+
.cart-total { text-align: right; margin-top: 15px; font-weight: bold; font-size: 1.1rem; }
|
| 193 |
+
.formulate-order-button { width: 100%; padding: 12px; background-color: #28a745; color: white; border: none; border-radius: 6px; font-size: 1rem; font-weight: bold; cursor: pointer; margin-top: 15px; }
|
| 194 |
</style>
|
| 195 |
</head>
|
| 196 |
<body>
|
|
|
|
| 198 |
Категории
|
| 199 |
</div>
|
| 200 |
<div class="container">
|
| 201 |
+
<div class="search-container">
|
| 202 |
+
<form action="{{ url_for('search') }}" method="get">
|
| 203 |
+
<input type="search" name="q" class="search-bar" placeholder=" Поиск по всем товарам" style="font-family: Arial, FontAwesome;">
|
| 204 |
+
</form>
|
| 205 |
+
</div>
|
| 206 |
<div class="category-grid">
|
| 207 |
{% for category, count in categories_with_counts.items() %}
|
| 208 |
<a href="{{ url_for('product_list', category_name=category) }}" class="category-card">
|
|
|
|
| 212 |
{% endfor %}
|
| 213 |
</div>
|
| 214 |
</div>
|
| 215 |
+
<button id="cart-button" class="floating-cart-button">
|
| 216 |
+
<i class="fas fa-shopping-cart"></i>
|
| 217 |
+
<span class="cart-count" id="cart-count">0</span>
|
| 218 |
+
</button>
|
| 219 |
+
<div id="cartModal" class="modal">
|
| 220 |
+
<div class="modal-content">
|
| 221 |
+
<span class="close" id="closeCartModal">×</span>
|
| 222 |
+
<h2>Корзина</h2>
|
| 223 |
+
<div id="cart-content"></div>
|
| 224 |
+
<p class="cart-total">Итого: <span id="cart-total">0</span> {{ currency_code }}</p>
|
| 225 |
+
<button class="formulate-order-button" id="formulate-order-btn">Сформировать заказ</button>
|
| 226 |
+
</div>
|
| 227 |
+
</div>
|
| 228 |
+
<script>
|
| 229 |
+
const currencyCode = '{{ currency_code }}';
|
| 230 |
+
let cart = JSON.parse(localStorage.getItem('gippoCart') || '[]');
|
| 231 |
+
|
| 232 |
+
function updateUI() {
|
| 233 |
+
let totalItems = 0;
|
| 234 |
+
cart.forEach(item => {
|
| 235 |
+
totalItems += item.quantity;
|
| 236 |
+
});
|
| 237 |
+
const cartCountEl = document.getElementById('cart-count');
|
| 238 |
+
cartCountEl.textContent = totalItems;
|
| 239 |
+
cartCountEl.style.display = totalItems > 0 ? 'flex' : 'none';
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
const modal = document.getElementById('cartModal');
|
| 243 |
+
const cartButton = document.getElementById('cart-button');
|
| 244 |
+
const closeButton = document.getElementById('closeCartModal');
|
| 245 |
+
|
| 246 |
+
cartButton.onclick = function() {
|
| 247 |
+
renderCartModal();
|
| 248 |
+
modal.style.display = "block";
|
| 249 |
+
}
|
| 250 |
+
closeButton.onclick = function() {
|
| 251 |
+
modal.style.display = "none";
|
| 252 |
+
}
|
| 253 |
+
window.onclick = function(event) {
|
| 254 |
+
if (event.target == modal) {
|
| 255 |
+
modal.style.display = "none";
|
| 256 |
+
}
|
| 257 |
+
}
|
| 258 |
+
function renderCartModal() {
|
| 259 |
+
const cartContent = document.getElementById('cart-content');
|
| 260 |
+
const cartTotalEl = document.getElementById('cart-total');
|
| 261 |
+
let total = 0;
|
| 262 |
+
if (cart.length === 0) {
|
| 263 |
+
cartContent.innerHTML = '<p>Корзина пуста</p>';
|
| 264 |
+
cartTotalEl.textContent = '0';
|
| 265 |
+
} else {
|
| 266 |
+
cartContent.innerHTML = cart.map(item => {
|
| 267 |
+
const itemTotal = item.price * item.quantity;
|
| 268 |
+
total += itemTotal;
|
| 269 |
+
return `<div class="cart-item"><span class="cart-item-info">${item.name} x ${item.quantity}</span> <span>${itemTotal.toFixed(0)}</span></div>`;
|
| 270 |
+
}).join('');
|
| 271 |
+
cartTotalEl.textContent = total.toFixed(0);
|
| 272 |
+
}
|
| 273 |
+
}
|
| 274 |
+
document.getElementById('formulate-order-btn').addEventListener('click', () => {
|
| 275 |
+
if (cart.length === 0) {
|
| 276 |
+
alert("Корзина пуста!");
|
| 277 |
+
return;
|
| 278 |
+
}
|
| 279 |
+
fetch('/create_order', {
|
| 280 |
+
method: 'POST',
|
| 281 |
+
headers: { 'Content-Type': 'application/json' },
|
| 282 |
+
body: JSON.stringify({ cart: cart })
|
| 283 |
+
})
|
| 284 |
+
.then(response => response.json())
|
| 285 |
+
.then(data => {
|
| 286 |
+
if (data.order_id) {
|
| 287 |
+
localStorage.removeItem('gippoCart');
|
| 288 |
+
window.location.href = `/order/${data.order_id}`;
|
| 289 |
+
} else {
|
| 290 |
+
alert('Ошибка при создании заказа.');
|
| 291 |
+
}
|
| 292 |
+
})
|
| 293 |
+
.catch(error => alert('Ошибка сети.'));
|
| 294 |
+
});
|
| 295 |
+
document.addEventListener('DOMContentLoaded', updateUI);
|
| 296 |
+
</script>
|
| 297 |
</body>
|
| 298 |
</html>
|
| 299 |
'''
|
|
|
|
| 304 |
<head>
|
| 305 |
<meta charset="UTF-8">
|
| 306 |
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
| 307 |
+
<title>{{ title }}</title>
|
| 308 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
| 309 |
<style>
|
| 310 |
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 0; background-color: #f0f2f5; color: #1c1e21; padding-bottom: 80px; }
|
| 311 |
+
.header { display: flex; align-items: center; background-color: #fff; padding: 10px 16px; border-bottom: 1px solid #dddfe2; position: sticky; top: 0; z-index: 100; }
|
| 312 |
.header a { color: #007bff; text-decoration: none; font-size: 1.2rem; }
|
| 313 |
.header h1 { font-size: 1.2rem; font-weight: 600; margin: 0; position: absolute; left: 50%; transform: translateX(-50%); }
|
| 314 |
+
.search-container { padding: 8px 16px; background-color: #fff; border-bottom: 1px solid #dddfe2; }
|
| 315 |
+
.search-bar { width: 100%; padding: 10px; font-size: 1rem; border-radius: 20px; border: 1px solid #ccd0d5; box-sizing: border-box; }
|
| 316 |
.product-list { display: flex; flex-direction: column; gap: 8px; padding: 8px; }
|
| 317 |
+
.product-card { display: flex; align-items: center; padding: 12px; background-color: #fff; border: 1px solid #dddfe2; border-radius: 12px; gap: 12px; }
|
| 318 |
+
.product-card img { width: 80px; height: 80px; object-fit: cover; border-radius: 8px; flex-shrink: 0; cursor: pointer; }
|
| 319 |
.product-info { flex-grow: 1; }
|
| 320 |
.product-name { font-size: 1rem; font-weight: 500; margin: 0 0 4px 0; }
|
| 321 |
.product-price { font-size: 0.9rem; color: #606770; margin: 0; }
|
| 322 |
.quantity-selector { display: flex; align-items: center; gap: 10px; }
|
| 323 |
+
.quantity-btn { width: 32px; height: 32px; border: 1px solid #ccc; background-color: #f8f9fa; border-radius: 50%; font-size: 1.5rem; line-height: 1; color: #007bff; cursor: pointer; transition: background-color 0.2s; }
|
| 324 |
+
.quantity-btn:active { background-color: #e2e6ea; }
|
| 325 |
.quantity-input { width: 40px; text-align: center; border: 1px solid #ccc; border-radius: 4px; padding: 5px; font-size: 1rem; -moz-appearance: textfield; }
|
| 326 |
.quantity-input::-webkit-outer-spin-button, .quantity-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
|
| 327 |
.floating-cart-button { position: fixed; bottom: 20px; right: 20px; background-color: #007bff; color: white; width: 60px; height: 60px; border-radius: 50%; border: none; display: flex; align-items: center; justify-content: center; font-size: 1.5rem; box-shadow: 0 4px 12px rgba(0,0,0,0.15); cursor: pointer; z-index: 1000; }
|
| 328 |
.cart-count { position: absolute; top: 0; right: 0; background-color: #dc3545; color: white; font-size: 0.75rem; width: 20px; height: 20px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; }
|
| 329 |
.modal { display: none; position: fixed; z-index: 1001; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); }
|
| 330 |
+
.modal-content { background-color: #fefefe; margin: auto; padding: 20px; border: 1px solid #888; width: 90%; max-width: 500px; border-radius: 8px; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); }
|
| 331 |
.close { color: #aaa; float: right; font-size: 28px; font-weight: bold; cursor: pointer; }
|
| 332 |
.cart-item { display: flex; justify-content: space-between; align-items: center; padding: 10px 0; border-bottom: 1px solid #eee; }
|
| 333 |
.cart-item-info { font-size: 0.9rem; }
|
| 334 |
.cart-total { text-align: right; margin-top: 15px; font-weight: bold; font-size: 1.1rem; }
|
| 335 |
.formulate-order-button { width: 100%; padding: 12px; background-color: #28a745; color: white; border: none; border-radius: 6px; font-size: 1rem; font-weight: bold; cursor: pointer; margin-top: 15px; }
|
| 336 |
+
#gallery-modal { background-color: rgba(0,0,0,0.9); z-index: 2000; }
|
| 337 |
+
#gallery-modal .close { color: #fff; font-size: 40px; position: absolute; top: 15px; right: 35px; }
|
| 338 |
+
.gallery-content { position: relative; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; }
|
| 339 |
+
.gallery-content img { max-width: 90%; max-height: 80%; object-fit: contain; }
|
| 340 |
+
.gallery-nav { cursor: pointer; position: absolute; top: 50%; transform: translateY(-50%); color: white; font-size: 3rem; padding: 16px; user-select: none; }
|
| 341 |
+
#prev-btn { left: 0; }
|
| 342 |
+
#next-btn { right: 0; }
|
| 343 |
</style>
|
| 344 |
</head>
|
| 345 |
<body>
|
| 346 |
<div class="header">
|
| 347 |
<a href="{{ url_for('catalog') }}"><i class="fas fa-chevron-left"></i></a>
|
| 348 |
+
<h1>{{ title }}</h1>
|
| 349 |
+
</div>
|
| 350 |
+
<div class="search-container">
|
| 351 |
+
<input type="search" id="product-search" class="search-bar" placeholder=" Поиск в этой категории..." style="font-family: Arial, FontAwesome;">
|
| 352 |
</div>
|
| 353 |
<div class="product-list" id="product-list">
|
| 354 |
+
{% if not products %}
|
| 355 |
+
<p style="text-align:center; padding: 20px;">Товары не найдены.</p>
|
| 356 |
+
{% endif %}
|
| 357 |
{% for product in products %}
|
| 358 |
+
<div class="product-card" data-product-id="{{ product.product_id }}" data-product-name="{{ product.name }}" data-product-desc="{{ product.description }}">
|
| 359 |
{% if product.get('photos') and product['photos']|length > 0 %}
|
| 360 |
+
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product['photos'][0] }}" alt="{{ product.name }}" class="product-image" data-photos='{{ product.photos|tojson }}'>
|
| 361 |
{% else %}
|
| 362 |
<img src="https://via.placeholder.com/80x80.png?text=N/A" alt="No Image">
|
| 363 |
{% endif %}
|
|
|
|
| 378 |
<i class="fas fa-shopping-cart"></i>
|
| 379 |
<span class="cart-count" id="cart-count">0</span>
|
| 380 |
</button>
|
|
|
|
| 381 |
<div id="cartModal" class="modal">
|
| 382 |
<div class="modal-content">
|
| 383 |
<span class="close" id="closeCartModal">×</span>
|
|
|
|
| 387 |
<button class="formulate-order-button" id="formulate-order-btn">Сформировать заказ</button>
|
| 388 |
</div>
|
| 389 |
</div>
|
| 390 |
+
<div id="gallery-modal" class="modal">
|
| 391 |
+
<span class="close" id="closeGalleryModal">×</span>
|
| 392 |
+
<div class="gallery-content">
|
| 393 |
+
<span class="gallery-nav" id="prev-btn">❮</span>
|
| 394 |
+
<img id="gallery-image" src="">
|
| 395 |
+
<span class="gallery-nav" id="next-btn">❯</span>
|
| 396 |
+
</div>
|
| 397 |
+
</div>
|
| 398 |
|
| 399 |
<script>
|
| 400 |
const allProducts = {{ products_json|safe }};
|
|
|
|
| 409 |
function updateCart(productId, change) {
|
| 410 |
const product = getProductById(productId);
|
| 411 |
if (!product) return;
|
|
|
|
| 412 |
const existingItemIndex = cart.findIndex(item => item.product_id === productId);
|
| 413 |
+
let newQuantity;
|
|
|
|
| 414 |
if (existingItemIndex > -1) {
|
| 415 |
newQuantity = cart[existingItemIndex].quantity + change;
|
| 416 |
} else {
|
| 417 |
+
newQuantity = change > 0 ? change : 0;
|
| 418 |
}
|
|
|
|
| 419 |
if (newQuantity > 0) {
|
| 420 |
if (existingItemIndex > -1) {
|
| 421 |
cart[existingItemIndex].quantity = newQuantity;
|
| 422 |
} else {
|
| 423 |
+
cart.push({ product_id: product.product_id, name: product.name, price: product.price, photo: product.photos && product.photos.length > 0 ? product.photos[0] : null, quantity: newQuantity });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 424 |
}
|
| 425 |
+
} else if (existingItemIndex > -1) {
|
| 426 |
+
cart.splice(existingItemIndex, 1);
|
| 427 |
}
|
|
|
|
| 428 |
localStorage.setItem('gippoCart', JSON.stringify(cart));
|
| 429 |
updateUI();
|
| 430 |
}
|
|
|
|
| 438 |
}
|
| 439 |
totalItems += item.quantity;
|
| 440 |
});
|
|
|
|
| 441 |
document.querySelectorAll('.product-card').forEach(card => {
|
| 442 |
const productId = card.dataset.productId;
|
| 443 |
if (!cart.some(item => item.product_id === productId)) {
|
| 444 |
card.querySelector('.quantity-input').value = 0;
|
| 445 |
}
|
| 446 |
});
|
|
|
|
| 447 |
const cartCountEl = document.getElementById('cart-count');
|
| 448 |
cartCountEl.textContent = totalItems;
|
| 449 |
cartCountEl.style.display = totalItems > 0 ? 'flex' : 'none';
|
|
|
|
| 460 |
}
|
| 461 |
});
|
| 462 |
|
| 463 |
+
const cartModal = document.getElementById('cartModal');
|
| 464 |
const cartButton = document.getElementById('cart-button');
|
| 465 |
+
const closeCartButton = document.getElementById('closeCartModal');
|
| 466 |
+
cartButton.onclick = function() { renderCartModal(); cartModal.style.display = "block"; }
|
| 467 |
+
closeCartButton.onclick = function() { cartModal.style.display = "none"; }
|
| 468 |
+
window.addEventListener('click', function(event) {
|
| 469 |
+
if (event.target == cartModal) { cartModal.style.display = "none"; }
|
| 470 |
+
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 471 |
|
| 472 |
function renderCartModal() {
|
| 473 |
const cartContent = document.getElementById('cart-content');
|
|
|
|
| 487 |
}
|
| 488 |
|
| 489 |
document.getElementById('formulate-order-btn').addEventListener('click', () => {
|
| 490 |
+
if (cart.length === 0) { alert("Корзина пуста!"); return; }
|
| 491 |
+
fetch('/create_order', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ cart: cart }) })
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 492 |
.then(response => response.json())
|
| 493 |
.then(data => {
|
| 494 |
+
if (data.order_id) { localStorage.removeItem('gippoCart'); window.location.href = `/order/${data.order_id}`; }
|
| 495 |
+
else { alert('Ошибка при создании заказа.'); }
|
| 496 |
+
}).catch(error => alert('Ошибка сети.'));
|
| 497 |
+
});
|
| 498 |
+
|
| 499 |
+
document.getElementById('product-search').addEventListener('input', function(e) {
|
| 500 |
+
const searchTerm = e.target.value.toLowerCase();
|
| 501 |
+
document.querySelectorAll('.product-card').forEach(card => {
|
| 502 |
+
const name = card.dataset.productName.toLowerCase();
|
| 503 |
+
const desc = card.dataset.productDesc.toLowerCase();
|
| 504 |
+
if (name.includes(searchTerm) || desc.includes(searchTerm)) {
|
| 505 |
+
card.style.display = 'flex';
|
| 506 |
} else {
|
| 507 |
+
card.style.display = 'none';
|
| 508 |
}
|
| 509 |
+
});
|
| 510 |
+
});
|
| 511 |
+
|
| 512 |
+
const galleryModal = document.getElementById('gallery-modal');
|
| 513 |
+
const galleryImage = document.getElementById('gallery-image');
|
| 514 |
+
const closeGalleryButton = document.getElementById('closeGalleryModal');
|
| 515 |
+
const prevBtn = document.getElementById('prev-btn');
|
| 516 |
+
const nextBtn = document.getElementById('next-btn');
|
| 517 |
+
let currentPhotos = [];
|
| 518 |
+
let currentIndex = 0;
|
| 519 |
+
|
| 520 |
+
function updateGalleryImage() {
|
| 521 |
+
if (currentPhotos.length > 0) {
|
| 522 |
+
galleryImage.src = `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${currentPhotos[currentIndex]}`;
|
| 523 |
+
prevBtn.style.display = currentPhotos.length > 1 ? 'block' : 'none';
|
| 524 |
+
nextBtn.style.display = currentPhotos.length > 1 ? 'block' : 'none';
|
| 525 |
+
}
|
| 526 |
+
}
|
| 527 |
+
function showNext() { currentIndex = (currentIndex + 1) % currentPhotos.length; updateGalleryImage(); }
|
| 528 |
+
function showPrev() { currentIndex = (currentIndex - 1 + currentPhotos.length) % currentPhotos.length; updateGalleryImage(); }
|
| 529 |
+
|
| 530 |
+
document.getElementById('product-list').addEventListener('click', e => {
|
| 531 |
+
if (e.target.classList.contains('product-image')) {
|
| 532 |
+
const photos = JSON.parse(e.target.dataset.photos);
|
| 533 |
+
if (photos && photos.length > 0) {
|
| 534 |
+
currentPhotos = photos;
|
| 535 |
+
currentIndex = 0;
|
| 536 |
+
updateGalleryImage();
|
| 537 |
+
galleryModal.style.display = 'block';
|
| 538 |
+
}
|
| 539 |
+
}
|
| 540 |
});
|
| 541 |
+
|
| 542 |
+
closeGalleryButton.onclick = () => { galleryModal.style.display = 'none'; };
|
| 543 |
+
nextBtn.onclick = showNext;
|
| 544 |
+
prevBtn.onclick = showPrev;
|
| 545 |
+
let touchstartX = 0;
|
| 546 |
+
let touchendX = 0;
|
| 547 |
+
galleryModal.addEventListener('touchstart', e => { touchstartX = e.changedTouches[0].screenX; }, false);
|
| 548 |
+
galleryModal.addEventListener('touchend', e => { touchendX = e.changedTouches[0].screenX; handleSwipe(); }, false);
|
| 549 |
+
function handleSwipe() {
|
| 550 |
+
if (touchendX < touchstartX) showNext();
|
| 551 |
+
if (touchendX > touchstartX) showPrev();
|
| 552 |
+
}
|
| 553 |
|
| 554 |
document.addEventListener('DOMContentLoaded', updateUI);
|
| 555 |
</script>
|
|
|
|
| 566 |
<title>Накладная №{{ order.id }}</title>
|
| 567 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
| 568 |
<style>
|
| 569 |
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; margin: 0; background-color: #f0f2f5; padding-bottom: 100px; }
|
| 570 |
.invoice-box { max-width: 800px; margin: 20px auto; padding: 20px; background: #fff; border: 1px solid #eee; box-shadow: 0 0 10px rgba(0, 0, 0, 0.15); font-size: 16px; line-height: 24px; color: #555; }
|
| 571 |
.invoice-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 20px; }
|
| 572 |
.invoice-header h1 { font-size: 2em; margin: 0; font-weight: 600; color: #333; }
|
|
|
|
| 579 |
.invoice-table .center { text-align: center; }
|
| 580 |
.invoice-table .right { text-align: right; }
|
| 581 |
.total-row td { border-top: 2px solid #333; font-weight: bold; }
|
| 582 |
+
.floating-buttons { position: fixed; bottom: 0; left: 0; width: 100%; background-color: #fff; box-shadow: 0 -2px 10px rgba(0,0,0,0.1); display: flex; justify-content: space-around; padding: 15px 10px; box-sizing: border-box; z-index: 100; }
|
| 583 |
+
.action-button { padding: 12px 20px; border: none; border-radius: 25px; color: white; font-weight: bold; cursor: pointer; transition: background-color 0.3s; font-size: 1rem; display: inline-flex; align-items: center; gap: 8px; text-decoration: none; box-shadow: 0 4px 10px rgba(0,0,0,0.2); flex-grow: 1; justify-content: center; margin: 0 5px; }
|
| 584 |
.whatsapp-btn { background-color: #25D366; }
|
| 585 |
.print-btn { background-color: #007BFF; }
|
| 586 |
@media print {
|
| 587 |
+
body { background-color: #fff; margin: 0; padding-bottom: 0; }
|
| 588 |
+
.invoice-box { box-shadow: none; border: none; margin: 0 auto; max-width: 100%; }
|
| 589 |
.floating-buttons { display: none; }
|
| 590 |
}
|
| 591 |
</style>
|
|
|
|
| 632 |
</tfoot>
|
| 633 |
</table>
|
| 634 |
</div>
|
|
|
|
| 635 |
<div class="floating-buttons">
|
| 636 |
<button class="action-button print-btn" onclick="window.print()"><i class="fas fa-print"></i> Печать</button>
|
| 637 |
<button class="action-button whatsapp-btn" onclick="sendOrderViaWhatsApp()"><i class="fab fa-whatsapp"></i> WhatsApp</button>
|
| 638 |
</div>
|
|
|
|
| 639 |
<script>
|
| 640 |
function sendOrderViaWhatsApp() {
|
| 641 |
const orderId = "{{ order.id }}";
|
|
|
|
| 671 |
<meta charset="UTF-8">
|
| 672 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 673 |
<title>Админ-панель</title>
|
|
|
|
| 674 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
| 675 |
<style>
|
| 676 |
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background-color: #f4f6f9; color: #333; margin: 0; padding: 15px; }
|
| 677 |
+
.container { max-width: 900px; margin: 0 auto; background-color: #fff; padding: 20px; border-radius: 10px; box-shadow: 0 3px 10px rgba(0,0,0,0.05); }
|
| 678 |
+
.header { display: flex; justify-content: space-between; align-items: center; padding-bottom: 15px; margin-bottom: 20px; border-bottom: 1px solid #e0e0e0; }
|
| 679 |
+
h1, h2 { color: #003C43; }
|
| 680 |
+
h1 { font-size: 1.6rem; font-weight: 600; }
|
| 681 |
+
h2 { font-size: 1.4rem; margin-top: 25px; margin-bottom: 15px; }
|
| 682 |
+
.section { margin-bottom: 25px; }
|
| 683 |
+
label { font-weight: 500; margin-top: 10px; display: block; color: #555; font-size: 0.9rem; }
|
| 684 |
+
input[type="text"], input[type="number"], textarea, select, .search-bar { width: 100%; padding: 10px; margin-top: 5px; border: 1px solid #ccc; border-radius: 6px; box-sizing: border-box; }
|
| 685 |
+
.search-bar { margin-bottom: 15px; }
|
| 686 |
+
button, .button { padding: 10px 18px; border: none; border-radius: 6px; background-color: #007bff; color: white; font-weight: 600; cursor: pointer; transition: background-color 0.3s; margin-top: 15px; font-size: 0.95rem; text-decoration: none; }
|
| 687 |
+
button:hover, .button:hover { background-color: #0056b3; }
|
| 688 |
+
.delete-btn { background-color: #dc3545; }
|
| 689 |
+
.delete-btn:hover { background-color: #c82333; }
|
| 690 |
+
.edit-btn { background-color: #ffc107; color: #212529; }
|
| 691 |
+
.edit-btn:hover { background-color: #e0a800; }
|
| 692 |
+
.message { padding: 12px 15px; border-radius: 6px; margin-bottom: 20px; font-size: 0.95rem; }
|
|
|
|
|
|
|
| 693 |
.message.success { background-color: #d4edda; color: #155724; }
|
| 694 |
.message.error { background-color: #f8d7da; color: #721c24; }
|
| 695 |
+
.category-block { margin-bottom: 20px; border: 1px solid #e0e0e0; border-radius: 8px; }
|
| 696 |
+
.category-header { display: flex; justify-content: space-between; align-items: center; padding: 15px; background-color: #f8f9fa; border-bottom: 1px solid #e0e0e0; }
|
| 697 |
+
.category-header h3 { margin: 0; font-size: 1.2rem; }
|
| 698 |
+
.category-content { padding: 15px; }
|
| 699 |
+
.product-item { display: flex; align-items: center; gap: 15px; padding: 10px 0; border-bottom: 1px solid #f0f0f0; }
|
| 700 |
+
.product-item:last-child { border-bottom: none; }
|
| 701 |
+
.product-item img { width: 50px; height: 50px; object-fit: cover; border-radius: 5px; }
|
| 702 |
+
.product-info { flex-grow: 1; }
|
| 703 |
+
.product-actions button, .product-actions .button { margin: 0 0 0 5px; padding: 6px 10px; font-size: 0.8rem; }
|
| 704 |
+
.modal { display: none; position: fixed; z-index: 1050; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.5); }
|
| 705 |
+
.modal-content { background-color: #fefefe; margin: 10% auto; padding: 20px; border: 1px solid #888; width: 90%; max-width: 500px; border-radius: 8px; }
|
| 706 |
+
.modal-header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #e5e5e5; padding-bottom: 10px; margin-bottom: 15px; }
|
| 707 |
+
.modal-header h2 { margin: 0; }
|
| 708 |
+
.close { color: #aaa; font-size: 28px; font-weight: bold; cursor: pointer; }
|
| 709 |
</style>
|
| 710 |
</head>
|
| 711 |
<body>
|
| 712 |
<div class="container">
|
| 713 |
<div class="header">
|
| 714 |
<h1>Админ-панель</h1>
|
| 715 |
+
<a href="{{ url_for('catalog') }}" class="button" style="margin-top:0;">В каталог</a>
|
| 716 |
</div>
|
|
|
|
| 717 |
{% with messages = get_flashed_messages(with_categories=true) %}
|
| 718 |
{% if messages %}
|
| 719 |
{% for category, message in messages %}
|
|
|
|
| 721 |
{% endfor %}
|
| 722 |
{% endif %}
|
| 723 |
{% endwith %}
|
|
|
|
| 724 |
<div class="section">
|
| 725 |
<h2>Управление категориями</h2>
|
| 726 |
+
<form method="POST" style="display:flex; gap: 10px; align-items: flex-end;">
|
| 727 |
+
<input type="hidden" name="action" value="add_category">
|
| 728 |
+
<div style="flex-grow:1;"><label for="category_name">Новая категория:</label><input type="text" id="category_name" name="category_name" required></div>
|
| 729 |
+
<button type="submit" style="margin-top:0;">Добавить</button>
|
| 730 |
+
</form>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 731 |
</div>
|
|
|
|
| 732 |
<div class="section">
|
| 733 |
+
<h2>Товары</h2>
|
| 734 |
+
<input type="search" id="admin-search" class="search-bar" placeholder=" Поиск по всем товарам..." style="font-family: Arial, FontAwesome;">
|
| 735 |
{% for category in categories %}
|
| 736 |
+
<div class="category-block" data-category-name="{{ category }}">
|
| 737 |
+
<div class="category-header">
|
| 738 |
+
<h3>{{ category }}</h3>
|
| 739 |
+
<div style="display:flex; align-items: center; gap: 10px;">
|
| 740 |
+
<button class="button add-product-btn" data-category="{{ category }}" style="margin-top:0; padding: 8px 12px;">+ Добавить товар</button>
|
| 741 |
+
<form method="POST" onsubmit="return confirm('Удалить категорию? Товары не будут удалены.');" style="margin:0;">
|
| 742 |
+
<input type="hidden" name="action" value="delete_category">
|
| 743 |
+
<input type="hidden" name="category_name" value="{{ category }}">
|
| 744 |
+
<button type="submit" class="delete-btn" style="margin:0; padding: 8px 12px;"><i class="fas fa-trash"></i></button>
|
| 745 |
+
</form>
|
| 746 |
+
</div>
|
| 747 |
+
</div>
|
| 748 |
<div class="category-content">
|
| 749 |
{% set products_in_cat = products_by_category.get(category, []) %}
|
| 750 |
{% if products_in_cat %}
|
| 751 |
{% for product in products_in_cat %}
|
| 752 |
+
<div class="product-item" data-product-name="{{ product.name }}" data-product-desc="{{ product.description }}" data-product-id="{{ product.product_id }}" data-product-info='{{ product|tojson }}'>
|
| 753 |
+
<img src="{% if product.photos %}{{ hf_url }}/photos/{{ product.photos[0] }}{% else %}https://via.placeholder.com/50x50.png?text=N/A{% endif %}" alt="{{ product.name }}">
|
|
|
|
|
|
|
|
|
|
|
|
|
| 754 |
<div class="product-info">
|
| 755 |
<strong>{{ product.name }}</strong><br>
|
| 756 |
+
<small>{{ "%.0f"|format(product.price) }} {{ currency_code }}</small>
|
| 757 |
+
</div>
|
| 758 |
+
<div class="product-actions">
|
| 759 |
+
<button class="button edit-btn edit-product-btn">Редактировать</button>
|
| 760 |
+
<form method="POST" onsubmit="return confirm('Удалить товар?');" style="margin:0; display:inline-block;">
|
| 761 |
+
<input type="hidden" name="action" value="delete_product">
|
| 762 |
+
<input type="hidden" name="product_id" value="{{ product.product_id }}">
|
| 763 |
+
<button type="submit" class="delete-btn">Удалить</button>
|
| 764 |
+
</form>
|
| 765 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 766 |
</div>
|
| 767 |
{% endfor %}
|
| 768 |
{% else %}
|
| 769 |
<p>В этой категории нет товаров.</p>
|
| 770 |
{% endif %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 771 |
</div>
|
| 772 |
+
</div>
|
| 773 |
{% endfor %}
|
| 774 |
</div>
|
| 775 |
</div>
|
| 776 |
+
|
| 777 |
+
<div id="productModal" class="modal">
|
| 778 |
+
<div class="modal-content">
|
| 779 |
+
<div class="modal-header">
|
| 780 |
+
<h2 id="modalTitle">Добавить товар</h2>
|
| 781 |
+
<span class="close">×</span>
|
| 782 |
+
</div>
|
| 783 |
+
<form id="productForm" method="POST" enctype="multipart/form-data">
|
| 784 |
+
<input type="hidden" name="action" id="formAction" value="add_product">
|
| 785 |
+
<input type="hidden" name="product_id" id="formProductId">
|
| 786 |
+
<input type="hidden" name="category" id="formCategory">
|
| 787 |
+
<label>Название:</label><input type="text" name="name" id="formName" required>
|
| 788 |
+
<label>Цена:</label><input type="number" name="price" id="formPrice" step="0.01" required>
|
| 789 |
+
<label>Описание:</label><textarea name="description" id="formDescription" rows="3"></textarea>
|
| 790 |
+
<label>Категория:</label>
|
| 791 |
+
<select name="new_category" id="formNewCategory" required>
|
| 792 |
+
{% for category in categories %}
|
| 793 |
+
<option value="{{ category }}">{{ category }}</option>
|
| 794 |
+
{% endfor %}
|
| 795 |
+
</select>
|
| 796 |
+
<label>Фото (до 10):</label><input type="file" name="photos" multiple>
|
| 797 |
+
<div id="existing-photos"></div>
|
| 798 |
+
<button type="submit">Сохранить</button>
|
| 799 |
+
</form>
|
| 800 |
+
</div>
|
| 801 |
+
</div>
|
| 802 |
+
|
| 803 |
+
<script>
|
| 804 |
+
const modal = document.getElementById('productModal');
|
| 805 |
+
const closeModal = modal.querySelector('.close');
|
| 806 |
+
|
| 807 |
+
closeModal.onclick = () => { modal.style.display = "none"; };
|
| 808 |
+
window.onclick = (event) => { if (event.target == modal) { modal.style.display = "none"; } };
|
| 809 |
+
|
| 810 |
+
document.querySelectorAll('.add-product-btn').forEach(button => {
|
| 811 |
+
button.addEventListener('click', function() {
|
| 812 |
+
const category = this.dataset.category;
|
| 813 |
+
document.getElementById('modalTitle').innerText = 'Добавить товар в "' + category + '"';
|
| 814 |
+
document.getElementById('productForm').reset();
|
| 815 |
+
document.getElementById('formAction').value = 'add_product';
|
| 816 |
+
document.getElementById('formProductId').value = '';
|
| 817 |
+
document.getElementById('formCategory').value = category;
|
| 818 |
+
document.getElementById('formNewCategory').value = category;
|
| 819 |
+
document.getElementById('existing-photos').innerHTML = '';
|
| 820 |
+
modal.style.display = 'block';
|
| 821 |
+
});
|
| 822 |
+
});
|
| 823 |
+
|
| 824 |
+
document.querySelectorAll('.edit-product-btn').forEach(button => {
|
| 825 |
+
button.addEventListener('click', function() {
|
| 826 |
+
const productItem = this.closest('.product-item');
|
| 827 |
+
const product = JSON.parse(productItem.dataset.productInfo);
|
| 828 |
+
|
| 829 |
+
document.getElementById('modalTitle').innerText = 'Редактировать товар';
|
| 830 |
+
document.getElementById('productForm').reset();
|
| 831 |
+
document.getElementById('formAction').value = 'edit_product';
|
| 832 |
+
document.getElementById('formProductId').value = product.product_id;
|
| 833 |
+
document.getElementById('formName').value = product.name;
|
| 834 |
+
document.getElementById('formPrice').value = product.price;
|
| 835 |
+
document.getElementById('formDescription').value = product.description || '';
|
| 836 |
+
document.getElementById('formNewCategory').value = product.category;
|
| 837 |
+
|
| 838 |
+
const photosContainer = document.getElementById('existing-photos');
|
| 839 |
+
photosContainer.innerHTML = '';
|
| 840 |
+
if (product.photos && product.photos.length > 0) {
|
| 841 |
+
let photoHtml = '<p>Текущие фото:</p>';
|
| 842 |
+
product.photos.forEach(p => {
|
| 843 |
+
photoHtml += `
|
| 844 |
+
<div style="display: inline-block; margin-right: 10px;">
|
| 845 |
+
<img src="${'{{ hf_url }}'}/photos/${p}" width="50" height="50">
|
| 846 |
+
<input type="checkbox" name="delete_photos" value="${p}"> Удалить
|
| 847 |
+
</div>`;
|
| 848 |
+
});
|
| 849 |
+
photosContainer.innerHTML = photoHtml;
|
| 850 |
+
}
|
| 851 |
+
|
| 852 |
+
modal.style.display = 'block';
|
| 853 |
+
});
|
| 854 |
+
});
|
| 855 |
+
|
| 856 |
+
document.getElementById('admin-search').addEventListener('input', function(e) {
|
| 857 |
+
const searchTerm = e.target.value.toLowerCase();
|
| 858 |
+
document.querySelectorAll('.category-block').forEach(catBlock => {
|
| 859 |
+
let hasVisibleProducts = false;
|
| 860 |
+
catBlock.querySelectorAll('.product-item').forEach(card => {
|
| 861 |
+
const name = card.dataset.productName.toLowerCase();
|
| 862 |
+
const desc = card.dataset.productDesc.toLowerCase();
|
| 863 |
+
if (name.includes(searchTerm) || desc.includes(searchTerm)) {
|
| 864 |
+
card.style.display = 'flex';
|
| 865 |
+
hasVisibleProducts = true;
|
| 866 |
+
} else {
|
| 867 |
+
card.style.display = 'none';
|
| 868 |
+
}
|
| 869 |
+
});
|
| 870 |
+
const categoryName = catBlock.dataset.categoryName.toLowerCase();
|
| 871 |
+
if (hasVisibleProducts || categoryName.includes(searchTerm)) {
|
| 872 |
+
catBlock.style.display = 'block';
|
| 873 |
+
} else {
|
| 874 |
+
catBlock.style.display = 'none';
|
| 875 |
+
}
|
| 876 |
+
});
|
| 877 |
+
});
|
| 878 |
+
</script>
|
| 879 |
</body>
|
| 880 |
</html>
|
| 881 |
'''
|
| 882 |
|
|
|
|
| 883 |
@app.route('/')
|
| 884 |
def catalog():
|
| 885 |
data = load_data()
|
|
|
|
| 893 |
else:
|
| 894 |
if cat not in categories:
|
| 895 |
categories_with_counts[cat] = 1
|
|
|
|
| 896 |
sorted_categories = dict(sorted(categories_with_counts.items()))
|
| 897 |
+
return render_template_string(
|
| 898 |
+
CATEGORY_PAGE_TEMPLATE,
|
| 899 |
+
categories_with_counts=sorted_categories,
|
| 900 |
+
currency_code=CURRENCY_CODE
|
| 901 |
+
)
|
| 902 |
|
| 903 |
@app.route('/category/<category_name>')
|
| 904 |
def product_list(category_name):
|
|
|
|
| 907 |
return render_template_string(
|
| 908 |
PRODUCT_PAGE_TEMPLATE,
|
| 909 |
products=products_in_category,
|
| 910 |
+
title=category_name,
|
| 911 |
products_json=json.dumps(products_in_category),
|
| 912 |
repo_id=REPO_ID,
|
| 913 |
currency_code=CURRENCY_CODE
|
| 914 |
)
|
| 915 |
|
| 916 |
+
@app.route('/search')
|
| 917 |
+
def search():
|
| 918 |
+
query = request.args.get('q', '').lower()
|
| 919 |
+
data = load_data()
|
| 920 |
+
all_products = data.get('products', [])
|
| 921 |
+
if not query:
|
| 922 |
+
search_results = all_products
|
| 923 |
+
else:
|
| 924 |
+
search_results = [
|
| 925 |
+
p for p in all_products
|
| 926 |
+
if query in p.get('name', '').lower() or query in p.get('description', '').lower()
|
| 927 |
+
]
|
| 928 |
+
return render_template_string(
|
| 929 |
+
PRODUCT_PAGE_TEMPLATE,
|
| 930 |
+
products=search_results,
|
| 931 |
+
title=f"Поиск: {request.args.get('q', '')}",
|
| 932 |
+
products_json=json.dumps(search_results),
|
| 933 |
+
repo_id=REPO_ID,
|
| 934 |
+
currency_code=CURRENCY_CODE
|
| 935 |
+
)
|
| 936 |
+
|
| 937 |
@app.route('/create_order', methods=['POST'])
|
| 938 |
def create_order():
|
| 939 |
order_data = request.get_json()
|
|
|
|
| 975 |
order = data.get('orders', {}).get(order_id)
|
| 976 |
return render_template_string(ORDER_PAGE_TEMPLATE, order=order, currency_code=CURRENCY_CODE, whatsapp_number=WHATSAPP_NUMBER)
|
| 977 |
|
| 978 |
+
def _handle_photo_upload(files, product_name):
|
| 979 |
+
photos_list = []
|
| 980 |
+
if not HF_TOKEN_WRITE:
|
| 981 |
+
return photos_list
|
| 982 |
+
api = HfApi()
|
| 983 |
+
for photo in files[:10]:
|
| 984 |
+
if photo and photo.filename:
|
| 985 |
+
try:
|
| 986 |
+
safe_name = secure_filename(product_name.replace(' ', '_'))[:50]
|
| 987 |
+
ext = os.path.splitext(photo.filename)[1].lower()
|
| 988 |
+
photo_filename = f"{safe_name}_{uuid4().hex[:8]}{ext}"
|
| 989 |
+
api.upload_file(
|
| 990 |
+
path_or_fileobj=photo.stream, path_in_repo=f"photos/{photo_filename}",
|
| 991 |
+
repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE
|
| 992 |
+
)
|
| 993 |
+
photos_list.append(photo_filename)
|
| 994 |
+
except Exception as e:
|
| 995 |
+
flash(f"Ошибка загрузки фото {photo.filename}: {e}", 'error')
|
| 996 |
+
return photos_list
|
| 997 |
+
|
| 998 |
@app.route('/admin', methods=['GET', 'POST'])
|
| 999 |
def admin():
|
| 1000 |
data = load_data()
|
| 1001 |
if request.method == 'POST':
|
| 1002 |
action = request.form.get('action')
|
| 1003 |
+
|
| 1004 |
if action == 'add_category':
|
| 1005 |
category_name = request.form.get('category_name', '').strip()
|
| 1006 |
if category_name and category_name not in data['categories']:
|
|
|
|
| 1009 |
flash('Категория добавлена.', 'success')
|
| 1010 |
else:
|
| 1011 |
flash('Ошибка: Неверное имя или категория уже существует.', 'error')
|
| 1012 |
+
|
| 1013 |
elif action == 'delete_category':
|
| 1014 |
category_to_delete = request.form.get('category_name')
|
| 1015 |
if category_to_delete in data['categories']:
|
|
|
|
| 1019 |
p['category'] = 'Без категории'
|
| 1020 |
save_data(data)
|
| 1021 |
flash('Категория удалена.', 'success')
|
| 1022 |
+
|
| 1023 |
elif action == 'add_product':
|
| 1024 |
name = request.form.get('name', '').strip()
|
| 1025 |
price_str = request.form.get('price', '').replace(',', '.')
|
|
|
|
| 1032 |
flash("Неверный формат цены.", 'error')
|
| 1033 |
return redirect(url_for('admin'))
|
| 1034 |
|
| 1035 |
+
photos_list = _handle_photo_upload(request.files.getlist('photos'), name)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1036 |
new_product = {
|
| 1037 |
'product_id': uuid4().hex, 'name': name, 'price': price,
|
| 1038 |
'description': request.form.get('description', '').strip(),
|
|
|
|
| 1042 |
save_data(data)
|
| 1043 |
flash('Товар добавлен.', 'success')
|
| 1044 |
|
| 1045 |
+
elif action == 'edit_product':
|
| 1046 |
+
product_id = request.form.get('product_id')
|
| 1047 |
+
product_to_edit = next((p for p in data['products'] if p.get('product_id') == product_id), None)
|
| 1048 |
+
if product_to_edit:
|
| 1049 |
+
name = request.form.get('name', '').strip()
|
| 1050 |
+
product_to_edit['name'] = name
|
| 1051 |
+
product_to_edit['price'] = round(float(request.form.get('price', '0').replace(',', '.')), 2)
|
| 1052 |
+
product_to_edit['description'] = request.form.get('description', '').strip()
|
| 1053 |
+
product_to_edit['category'] = request.form.get('new_category')
|
| 1054 |
+
|
| 1055 |
+
photos_to_delete = request.form.getlist('delete_photos')
|
| 1056 |
+
if photos_to_delete and HF_TOKEN_WRITE:
|
| 1057 |
+
try:
|
| 1058 |
+
api = HfApi()
|
| 1059 |
+
api.delete_files(repo_id=REPO_ID, paths_in_repo=[f"photos/{p}" for p in photos_to_delete], repo_type="dataset", token=HF_TOKEN_WRITE)
|
| 1060 |
+
product_to_edit['photos'] = [p for p in product_to_edit.get('photos', []) if p not in photos_to_delete]
|
| 1061 |
+
except Exception as e:
|
| 1062 |
+
flash(f"Не удалось удалить некоторые фото с сервера: {e}", "error")
|
| 1063 |
+
|
| 1064 |
+
new_photos = _handle_photo_upload(request.files.getlist('photos'), name)
|
| 1065 |
+
product_to_edit['photos'].extend(new_photos)
|
| 1066 |
+
|
| 1067 |
+
save_data(data)
|
| 1068 |
+
flash('Товар обновлен.', 'success')
|
| 1069 |
+
else:
|
| 1070 |
+
flash('Товар для редактирования не найден.', 'error')
|
| 1071 |
+
|
| 1072 |
elif action == 'delete_product':
|
| 1073 |
product_id = request.form.get('product_id')
|
| 1074 |
product_to_delete = next((p for p in data['products'] if p.get('product_id') == product_id), None)
|
|
|
|
| 1083 |
data['products'] = [p for p in data['products'] if p.get('product_id') != product_id]
|
| 1084 |
save_data(data)
|
| 1085 |
flash('Товар удален.', 'success')
|
| 1086 |
+
|
| 1087 |
return redirect(url_for('admin'))
|
| 1088 |
|
| 1089 |
products_by_category = {}
|
|
|
|
| 1098 |
categories=sorted(data.get('categories', [])),
|
| 1099 |
products_by_category=products_by_category,
|
| 1100 |
repo_id=REPO_ID,
|
| 1101 |
+
currency_code=CURRENCY_CODE,
|
| 1102 |
+
hf_url=f"https://huggingface.co/datasets/{REPO_ID}/resolve/main"
|
| 1103 |
)
|
| 1104 |
|
| 1105 |
if __name__ == '__main__':
|