flpolprojects commited on
Commit
38ba303
·
verified ·
1 Parent(s): 090b832

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1041 -695
app.py CHANGED
@@ -1,4 +1,4 @@
1
- from flask import Flask, render_template_string, request, redirect, url_for
2
  import json
3
  import os
4
  import logging
@@ -6,11 +6,13 @@ import threading
6
  import time
7
  from datetime import datetime
8
  from huggingface_hub import HfApi, hf_hub_download
9
- from huggingface_hub.utils import RepositoryNotFoundError
10
  from werkzeug.utils import secure_filename
 
11
 
12
  app = Flask(__name__)
13
  DATA_FILE = 'dataasdem.json'
 
14
 
15
  REPO_ID = "flpolprojects/dataasdem"
16
  HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
@@ -27,19 +29,46 @@ def load_data():
27
  data = json.load(file)
28
  logging.info("Данные успешно загружены из JSON")
29
  if not isinstance(data, dict) or 'products' not in data or 'categories' not in data:
30
- return {'products': [], 'categories': [] if not isinstance(data, list) else data}
 
 
 
 
 
 
 
31
  return data
32
  except FileNotFoundError:
33
- logging.warning("Локальный файл базы данных не найден после скачивания.")
34
  return {'products': [], 'categories': []}
35
  except json.JSONDecodeError:
36
  logging.error("Ошибка: Невозможно декодировать JSON файл.")
37
  return {'products': [], 'categories': []}
38
  except RepositoryNotFoundError:
39
- logging.error("Репозиторий не найден. Создание локальной базы данных.")
40
  return {'products': [], 'categories': []}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  except Exception as e:
42
- logging.error(f"Произошла ошибка при загрузке данных: {e}")
43
  return {'products': [], 'categories': []}
44
 
45
  def save_data(data):
@@ -53,6 +82,9 @@ def save_data(data):
53
  raise
54
 
55
  def upload_db_to_hf():
 
 
 
56
  try:
57
  api = HfApi()
58
  api.upload_file(
@@ -65,9 +97,12 @@ def upload_db_to_hf():
65
  )
66
  logging.info("Резервная копия JSON базы успешно загружена на Hugging Face.")
67
  except Exception as e:
68
- logging.error(f"Ошибка при загрузке резервной копии: {e}")
69
 
70
  def download_db_from_hf():
 
 
 
71
  try:
72
  hf_hub_download(
73
  repo_id=REPO_ID,
@@ -78,493 +113,599 @@ def download_db_from_hf():
78
  local_dir_use_symlinks=False
79
  )
80
  logging.info("JSON база успешно скачана из Hugging Face.")
81
- except RepositoryNotFoundError as e:
82
- logging.error(f"Репозиторий не найден: {e}")
83
- raise
84
  except Exception as e:
85
- logging.error(f"Ошибка при скачивании JSON базы: {e}")
86
- raise
87
 
88
  def periodic_backup():
89
  while True:
90
  upload_db_to_hf()
91
  time.sleep(800)
92
 
93
- @app.route('/')
94
- def catalog():
95
- data = load_data()
96
- products = sorted(data['products'], key=lambda x: x.get('added_at', ''), reverse=True)
97
- categories = data['categories']
98
-
99
- catalog_html = '''
100
- <!DOCTYPE html>
101
- <html lang="ru">
102
- <head>
103
- <meta charset="UTF-8">
104
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
105
- <title>Asdem - нижнее белье оптом </title>
106
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
107
- <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
108
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.css">
109
- <style>
110
- :root {
111
- --primary-color: #F48FB1;
112
- --secondary-color: #CE93D8;
113
- --text-color: #333;
114
- --bg-color: #FCE4EC;
115
- --light-text: #fff;
116
- --shadow-color: rgba(0, 0, 0, 0.1);
117
- }
118
- * {
119
- margin: 0;
120
- padding: 0;
121
- box-sizing: border-box;
122
- }
123
- body {
124
- font-family: 'Poppins', sans-serif;
125
- background-color: var(--bg-color);
126
- color: var(--text-color);
127
- line-height: 1.6;
128
- transition: background-color 0.3s, color 0.3s;
129
- }
130
- .container {
131
- max-width: 1300px;
132
- margin: 0 auto;
133
- padding: 20px;
134
- }
135
- .header {
136
- display: flex;
137
- justify-content: space-between;
138
- align-items: center;
139
- padding: 15px 0;
140
- border-bottom: 1px solid var(--primary-color);
141
- }
142
- .header-logo {
143
- width: 60px;
144
- height: 60px;
145
- border-radius: 50%;
146
- object-fit: cover;
147
- box-shadow: 0 4px 15px var(--shadow-color);
148
- transition: transform 0.3s ease, box-shadow 0.3s ease;
149
- }
150
- .header-logo:hover {
151
- transform: scale(1.1);
152
- box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
153
- }
154
- .header h1 {
155
- font-size: 1.5rem;
156
- font-weight: 600;
157
- margin-left: 15px;
158
- color: var(--primary-color);
159
- }
160
- .filters-container {
161
- margin: 20px 0;
162
- display: flex;
163
- flex-wrap: wrap;
164
- gap: 10px;
165
- justify-content: center;
166
- }
167
- .search-container {
168
- margin: 20px 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
  text-align: center;
170
- }
171
- #search-input {
172
- width: 90%;
173
- max-width: 600px;
174
- padding: 12px 18px;
175
- font-size: 1rem;
176
- border: 1px solid var(--secondary-color);
177
- border-radius: 8px;
178
- outline: none;
179
- box-shadow: 0 2px 5px var(--shadow-color);
180
- transition: all 0.3s ease;
181
- }
182
- #search-input:focus {
183
- border-color: var(--primary-color);
184
- box-shadow: 0 4px 15px rgba(244, 143, 177, 0.3);
185
- }
186
- .category-filter {
187
- padding: 8px 16px;
188
- border: 1px solid var(--secondary-color);
189
- border-radius: 8px;
190
- background-color: var(--light-text);
191
- color: var(--text-color);
192
- cursor: pointer;
193
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
194
- font-size: 0.9rem;
195
- font-weight: 400;
196
- }
197
- .category-filter.active, .category-filter:hover {
198
- background-color: var(--primary-color);
199
- color: var(--light-text);
200
- border-color: var(--primary-color);
201
- box-shadow: 0 2px 10px rgba(244, 143, 177, 0.4);
202
- }
203
  .products-grid {
204
- display: grid;
205
- grid-template-columns: repeat(2, minmax(200px, 1fr));
206
- gap: 15px;
207
- padding: 10px;
208
- }
209
- .product {
210
- background: var(--light-text);
211
- border-radius: 15px;
212
- padding: 15px;
213
- box-shadow: 0 4px 15px var(--shadow-color);
214
- transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.3s ease;
215
- overflow: hidden;
216
- }
217
- .product:hover {
218
- transform: translateY(-5px) scale(1.02);
219
- box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
220
- }
221
- .product-image {
222
- width: 100%;
223
- aspect-ratio: 1;
224
- background-color: #fff;
225
- border-radius: 10px;
226
- overflow: hidden;
227
- display: flex;
228
- justify-content: center;
229
- align-items: center;
230
- }
231
- .product-image img {
232
- max-width: 100%;
233
- max-height: 100%;
234
- object-fit: contain;
235
- transition: transform 0.3s ease;
236
- }
237
- .product-image img:hover {
238
- transform: scale(1.1);
239
- }
240
- .product h2 {
241
- font-size: 1rem;
242
- font-weight: 600;
243
- margin: 10px 0;
244
- text-align: center;
245
- white-space: nowrap;
246
- overflow: hidden;
247
- text-overflow: ellipsis;
248
- }
249
- .product-price {
250
- font-size: 1.1rem;
251
- color: #ef4444;
252
- font-weight: 700;
253
- text-align: center;
254
- margin: 5px 0;
255
- }
256
- .product-description {
257
- font-size: 0.8rem;
258
- color: #718096;
259
- text-align: center;
260
- margin-bottom: 15px;
261
- overflow: hidden;
262
- text-overflow: ellipsis;
263
- white-space: nowrap;
264
- }
265
- .product-button {
266
- display: block;
267
- width: 100%;
268
- padding: 8px;
269
- border: none;
270
- border-radius: 8px;
271
- background-color: var(--primary-color);
272
- color: var(--light-text);
273
- font-size: 0.8rem;
274
- font-weight: 500;
275
- cursor: pointer;
276
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
277
- margin: 5px 0;
278
- text-align: center;
279
- text-decoration: none;
280
- }
281
- .product-button:hover {
282
- background-color: #E91E63;
283
- box-shadow: 0 4px 15px rgba(233, 30, 99, 0.4);
284
- transform: translateY(-2px);
285
- }
286
- .add-to-cart {
287
- background-color: var(--secondary-color);
288
- }
289
- .add-to-cart:hover {
290
- background-color: #BA68C8;
291
- box-shadow: 0 4px 15px rgba(186, 104, 200, 0.4);
292
- }
293
- #cart-button {
294
- position: fixed;
295
- bottom: 20px;
296
- right: 20px;
297
- background-color: #ef4444;
298
- color: var(--light-text);
299
- border: none;
300
- border-radius: 50%;
301
- width: 50px;
302
- height: 50px;
303
- font-size: 1.2rem;
304
- cursor: pointer;
305
- display: none;
306
- box-shadow: 0 4px 15px rgba(239, 68, 68, 0.4);
307
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
308
- z-index: 1000;
309
- }
310
- .modal {
311
- display: none;
312
- position: fixed;
313
- z-index: 1001;
314
- left: 0;
315
- top: 0;
316
- width: 100%;
317
- height: 100%;
318
- background-color: rgba(0,0,0,0.5);
319
- backdrop-filter: blur(5px);
320
- }
321
- .modal-content {
322
- background: var(--light-text);
323
- margin: 5% auto;
324
- padding: 20px;
325
- border-radius: 15px;
326
- width: 90%;
327
- max-width: 700px;
328
- box-shadow: 0 10px 30px rgba(0,0,0,0.2);
329
- animation: slideIn 0.3s ease-out;
330
- }
331
- @keyframes slideIn {
332
- from { transform: translateY(-50px); opacity: 0; }
333
- to { transform: translateY(0); opacity: 1; }
334
- }
335
- .close {
336
- float: right;
337
- font-size: 1.5rem;
338
- color: #718096;
339
- cursor: pointer;
340
- transition: color 0.3s;
341
- }
342
- .close:hover {
343
- color: var(--primary-color);
344
- }
345
- .cart-item {
346
- display: flex;
347
- justify-content: space-between;
348
- align-items: center;
349
- padding: 15px 0;
350
- border-bottom: 1px solid var(--secondary-color);
351
- }
352
- .cart-item img {
353
- width: 50px;
354
- height: 50px;
355
- object-fit: contain;
356
- border-radius: 8px;
357
- margin-right: 15px;
358
- }
359
- .quantity-input, .color-select {
360
- width: 100%;
361
- max-width: 150px;
362
- padding: 8px;
363
- border: 1px solid var(--secondary-color);
364
- border-radius: 8px;
365
- font-size: 1rem;
366
- margin: 5px 0;
367
- }
368
- .clear-cart {
369
- background-color: #ef4444;
370
- }
371
- .clear-cart:hover {
372
- background-color: #dc2626;
373
- box-shadow: 0 4px 15px rgba(220, 38, 38, 0.4);
374
- }
375
- .order-button {
376
- background-color: var(--secondary-color);
377
- }
378
- .order-button:hover {
379
- background-color: #BA68C8;
380
- box-shadow: 0 4px 15px rgba(186, 104, 200, 0.4);
381
- }
382
- @media (max-width: 768px) {
383
- .products-grid {
384
- grid-template-columns: repeat(2, minmax(150px, 1fr));
385
- }
386
- }
387
- </style>
388
- </head>
389
- <body>
390
- <div class="container">
391
- <div class="header">
392
- <img src="''' + LOGO_URL + '''" alt="Logo" class="header-logo">
393
- <h1>Каталог</h1>
394
- </div>
395
- <div class="filters-container">
396
- <button class="category-filter active" data-category="all">Все категории</button>
397
- {% for category in categories %}
398
- <button class="category-filter" data-category="{{ category }}">{{ category }}</button>
399
- {% endfor %}
400
- </div>
401
- <div class="search-container">
402
- <input type="text" id="search-input" placeholder="Поиск товаров...">
403
- </div>
404
- <div class="products-grid" id="products-grid">
405
- {% for product in products %}
406
- <div class="product"
407
- data-name="{{ product['name']|lower }}"
408
- data-description="{{ product['description']|lower }}"
409
- data-category="{{ product.get('category', 'Без категории') }}">
410
- {% if product.get('photos') and product['photos']|length > 0 %}
411
- <div class="product-image">
412
- <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product['photos'][0] }}"
413
- alt="{{ product['name'] }}"
414
- loading="lazy">
415
- </div>
416
- {% endif %}
417
- <h2>{{ product['name'] }}</h2>
418
- <div class="product-price">{{ product['price'] }} с</div>
419
- <p class="product-description">{{ product['description'][:50] }}{% if product['description']|length > 50 %}...{% endif %}</p>
420
- <button class="product-button" onclick="openModal({{ loop.index0 }})">Подробнее</button>
421
- <button class="product-button add-to-cart" onclick="openQuantityModal({{ loop.index0 }})">В корзину</button>
422
  </div>
423
- {% endfor %}
424
  </div>
 
425
  </div>
 
426
 
427
- <div id="productModal" class="modal">
428
- <div class="modal-content">
429
- <span class="close" onclick="closeModal('productModal')">×</span>
430
- <div id="modalContent"></div>
431
- </div>
432
  </div>
 
433
 
434
- <div id="quantityModal" class="modal">
435
- <div class="modal-content">
436
- <span class="close" onclick="closeModal('quantityModal')">×</span>
437
- <h2>Укажите количество и цвет</h2>
438
- <input type="number" id="quantityInput" class="quantity-input" min="1" value="1">
439
- <select id="colorSelect" class="color-select"></select>
440
- <button class="product-button" onclick="confirmAddToCart()">Добавить</button>
441
- </div>
 
442
  </div>
 
443
 
444
- <div id="cartModal" class="modal">
445
- <div class="modal-content">
446
- <span class="close" onclick="closeModal('cartModal')">×</span>
447
- <h2>Корзина</h2>
448
- <div id="cartContent"></div>
449
- <div style="margin-top: 20px; text-align: right;">
450
- <strong>Итого: <span id="cartTotal">0</span> с</strong>
451
- <button class="product-button clear-cart" onclick="clearCart()">Очистить</button>
452
- <button class="product-button order-button" onclick="orderViaWhatsApp()">Заказать</button>
453
- </div>
454
  </div>
455
  </div>
 
456
 
457
- <button id="cart-button" onclick="openCartModal()">🛒</button>
458
 
459
- <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
460
- <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.5.3/dist/umd/popper.min.js"></script>
461
- <script src="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.js"></script>
462
- <script>
463
- const products = {{ products|tojson }};
464
- let selectedProductIndex = null;
465
 
466
- function openModal(index) {
467
- loadProductDetails(index);
468
- document.getElementById('productModal').style.display = "block";
469
- }
470
 
471
- function closeModal(modalId) {
472
- document.getElementById(modalId).style.display = "none";
473
- }
474
 
475
- function loadProductDetails(index) {
476
- fetch('/product/' + index)
477
- .then(response => response.text())
478
- .then(data => {
479
- document.getElementById('modalContent').innerHTML = data;
480
- initializeSwiper();
481
- })
482
- .catch(error => console.error('Ошибка:', error));
483
- }
 
484
 
485
- function initializeSwiper() {
 
486
  new Swiper('.swiper-container', {
487
  slidesPerView: 1,
488
  spaceBetween: 20,
489
- loop: true,
490
  grabCursor: true,
491
  pagination: { el: '.swiper-pagination', clickable: true },
492
  navigation: { nextEl: '.swiper-button-next', prevEl: '.swiper-button-prev' },
493
- zoom: { maxRatio: 3 }
 
 
494
  });
 
 
 
 
 
 
 
 
 
 
 
495
  }
496
 
497
- function openQuantityModal(index) {
498
- selectedProductIndex = index;
499
- const product = products[index];
500
- const colorSelect = document.getElementById('colorSelect');
501
- colorSelect.innerHTML = '';
502
- if (product.colors && product.colors.length > 0) {
503
- product.colors.forEach(color => {
504
- const option = document.createElement('option');
505
- option.value = color;
506
- option.text = color;
507
- colorSelect.appendChild(option);
508
- });
509
- } else {
510
  const option = document.createElement('option');
511
- option.value = 'Нет цвета';
512
- option.text = 'Нет цвета';
513
  colorSelect.appendChild(option);
514
- }
515
- document.getElementById('quantityModal').style.display = 'block';
516
- document.getElementById('quantityInput').value = 1;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
517
  }
518
 
519
- function confirmAddToCart() {
520
- if (selectedProductIndex === null) return;
521
- const quantity = parseInt(document.getElementById('quantityInput').value) || 1;
522
- const color = document.getElementById('colorSelect').value;
523
- if (quantity <= 0) {
524
- alert("Укажите количество больше 0");
525
- return;
526
- }
527
- let cart = JSON.parse(localStorage.getItem('cart') || '[]');
528
- const product = products[selectedProductIndex];
529
- const cartItemId = `${product.name}-${color}`;
530
- const existingItem = cart.find(item => item.id === cartItemId);
531
-
532
- if (existingItem) {
533
- existingItem.quantity += quantity;
534
- } else {
535
- cart.push({
536
- id: cartItemId,
537
- name: product.name,
538
- price: product.price,
539
- photo: product.photos && product.photos.length > 0 ? product.photos[0] : '',
540
- quantity: quantity,
541
- color: color
542
- });
543
- }
544
-
545
- localStorage.setItem('cart', JSON.stringify(cart));
546
- closeModal('quantityModal');
547
- updateCartButton();
548
  }
549
 
550
- function updateCartButton() {
551
- const cart = JSON.parse(localStorage.getItem('cart') || '[]');
552
- document.getElementById('cart-button').style.display = cart.length > 0 ? 'block' : 'none';
 
 
 
 
 
 
 
 
 
 
 
553
  }
554
 
555
- function openCartModal() {
556
- const cart = JSON.parse(localStorage.getItem('cart') || '[]');
557
- const cartContent = document.getElementById('cartContent');
558
- let total = 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
559
 
560
- cartContent.innerHTML = cart.length === 0 ? '<p>Корзина пуста</p>' : cart.map(item => {
561
- const itemTotal = item.price * item.quantity;
 
 
 
 
 
 
 
 
562
  total += itemTotal;
 
563
  return `
564
  <div class="cart-item">
565
- <div style="display: flex; align-items: center;">
566
- ${item.photo ? `<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/${item.photo}" alt="${item.name}">` : ''}
567
- <div>
568
  <strong>${item.name}</strong>
569
  <p>${item.price} с × ${item.quantity} (Цвет: ${item.color})</p>
570
  </div>
@@ -573,119 +714,149 @@ def catalog():
573
  </div>
574
  `;
575
  }).join('');
576
-
577
- document.getElementById('cartTotal').textContent = total;
578
- document.getElementById('cartModal').style.display = 'block';
579
  }
580
 
581
- function orderViaWhatsApp() {
582
- const cart = JSON.parse(localStorage.getItem('cart') || '[]');
583
- if (cart.length === 0) {
584
- alert("Корзина пуста!");
585
- return;
586
- }
587
- let total = 0;
588
- let orderText = "Заказ:%0A";
589
- cart.forEach((item, index) => {
590
- const itemTotal = item.price * item.quantity;
591
- total += itemTotal;
592
- orderText += `${index + 1}. ${item.name} - ${item.price} с × ${item.quantity} (Цвет: ${item.color})%0A`;
593
- });
594
- orderText += `Итого: ${total} с`;
595
- window.open(`https://api.whatsapp.com/send?phone=996772179559&text=${orderText}`, '_blank');
596
- }
597
 
598
- function clearCart() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
599
  localStorage.removeItem('cart');
600
  closeModal('cartModal');
601
  updateCartButton();
602
  }
 
603
 
604
- window.onclick = function(event) {
605
- if (event.target.className === 'modal') event.target.style.display = "none";
 
606
  }
 
607
 
608
- document.getElementById('search-input').addEventListener('input', filterProducts);
609
- document.querySelectorAll('.category-filter').forEach(filter => {
610
- filter.addEventListener('click', function() {
611
- document.querySelectorAll('.category-filter').forEach(f => f.classList.remove('active'));
612
- this.classList.add('active');
613
- filterProducts();
614
- });
615
  });
 
616
 
617
- function filterProducts() {
618
- const searchTerm = document.getElementById('search-input').value.toLowerCase();
619
- const activeCategory = document.querySelector('.category-filter.active').dataset.category;
620
- document.querySelectorAll('.product').forEach(product => {
621
- const name = product.getAttribute('data-name');
622
- const description = product.getAttribute('data-description');
623
- const category = product.getAttribute('data-category');
624
- const matchesSearch = name.includes(searchTerm) || description.includes(searchTerm);
625
- const matchesCategory = activeCategory === 'all' || category === activeCategory;
626
- product.style.display = matchesSearch && matchesCategory ? 'block' : 'none';
627
- });
628
- }
629
 
630
- updateCartButton();
631
- </script>
632
- </body>
633
- </html>
634
- '''
635
- return render_template_string(catalog_html, products=products, categories=categories, repo_id=REPO_ID)
636
 
637
- @app.route('/product/<int:index>')
638
- def product_detail(index):
639
- data = load_data()
640
- products = data['products']
641
- try:
642
- product = products[index]
643
- except IndexError:
644
- return "Продукт не найден", 404
645
- detail_html = '''
646
- <div class="container" style="padding: 20px;">
647
- <h2 style="font-size: 1.8rem; font-weight: 600; margin-bottom: 20px;">{{ product['name'] }}</h2>
648
- <div class="swiper-container" style="max-width: 400px; margin: 0 auto 20px;">
649
- <div class="swiper-wrapper">
650
- {% if product.get('photos') %}
651
- {% for photo in product['photos'] %}
652
- <div class="swiper-slide" style="background-color: #fff; display: flex; justify-content: center; align-items: center;">
653
- <div class="swiper-zoom-container">
654
- <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo }}"
655
- alt="{{ product['name'] }}"
656
- style="max-width: 100%; max-height: 300px; object-fit: contain;">
657
- </div>
658
- </div>
659
- {% endfor %}
660
- {% else %}
661
- <div class="swiper-slide">
662
- <img src="https://via.placeholder.com/300" alt="No Image">
663
  </div>
664
- {% endif %}
665
  </div>
666
- <div class="swiper-pagination"></div>
667
- <div class="swiper-button-next"></div>
668
- <div class="swiper-button-prev"></div>
 
 
 
669
  </div>
670
- <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
671
- <p><strong>Цена:</strong> {{ product['price'] }} с</p>
672
- <p><strong>Описание:</strong> {{ product['description'] }}</p>
673
- <p><strong>Доступные цвета:</strong> {{ product.get('colors', ['Нет цветов'])|join(', ') }}</p>
 
674
  </div>
675
- '''
676
- return render_template_string(detail_html, product=product, repo_id=REPO_ID)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
677
 
678
  @app.route('/admin', methods=['GET', 'POST'])
679
  def admin():
680
  data = load_data()
681
- products = data['products']
682
- categories = data['categories']
683
 
684
  if request.method == 'POST':
685
  action = request.form.get('action')
686
-
687
  if action == 'add_category':
688
- category_name = request.form.get('category_name')
689
  if category_name and category_name not in categories:
690
  categories.append(category_name)
691
  save_data(data)
@@ -693,107 +864,174 @@ def admin():
693
  return "Ошибка: Категория уже существует или не указано название", 400
694
 
695
  elif action == 'delete_category':
696
- category_index = int(request.form.get('category_index'))
697
- deleted_category = categories.pop(category_index)
698
- for product in products:
699
- if product.get('category') == deleted_category:
700
- product['category'] = 'Без категории'
701
- save_data(data)
702
- return redirect(url_for('admin'))
 
 
 
 
 
 
 
 
703
 
704
  elif action == 'add':
705
- name = request.form.get('name')
706
- price = request.form.get('price')
707
- description = request.form.get('description')
708
  category = request.form.get('category')
709
  photos_files = request.files.getlist('photos')
710
- colors = request.form.getlist('colors')
 
 
 
 
 
 
 
 
 
711
  photos_list = []
712
-
713
  if photos_files:
714
- for photo in photos_files[:10]:
715
  if photo and photo.filename:
716
- photo_filename = secure_filename(photo.filename)
 
 
 
 
 
717
  uploads_dir = 'uploads'
718
  os.makedirs(uploads_dir, exist_ok=True)
719
  temp_path = os.path.join(uploads_dir, photo_filename)
720
- photo.save(temp_path)
721
- api = HfApi()
722
- api.upload_file(
723
- path_or_fileobj=temp_path,
724
- path_in_repo=f"photos/{photo_filename}",
725
- repo_id=REPO_ID,
726
- repo_type="dataset",
727
- token=HF_TOKEN_WRITE,
728
- commit_message=f"Добавлено фото для товара {name}"
729
- )
730
- photos_list.append(photo_filename)
731
- if os.path.exists(temp_path):
732
- os.remove(temp_path)
733
-
734
- if not name or not price or not description:
735
- return "Ошибка: Заполните все обязательные поля", 400
736
-
737
- price = float(price.replace(',', '.'))
 
 
 
 
738
  new_product = {
739
  'name': name,
740
  'price': price,
741
  'description': description,
742
  'category': category if category in categories else 'Без категории',
743
  'photos': photos_list,
744
- 'colors': colors if colors else [],
745
  'added_at': datetime.now().isoformat()
746
  }
747
  products.append(new_product)
748
  save_data(data)
749
  return redirect(url_for('admin'))
750
-
751
  elif action == 'edit':
752
- index = int(request.form.get('index'))
753
- name = request.form.get('name')
754
- price = request.form.get('price')
755
- description = request.form.get('description')
756
- category = request.form.get('category')
757
- photos_files = request.files.getlist('photos')
758
- colors = request.form.getlist('colors')
759
-
760
- if photos_files and any(photo.filename for photo in photos_files):
761
- new_photos_list = []
762
- for photo in photos_files[:10]:
763
- if photo and photo.filename:
764
- photo_filename = secure_filename(photo.filename)
765
- uploads_dir = 'uploads'
766
- os.makedirs(uploads_dir, exist_ok=True)
767
- temp_path = os.path.join(uploads_dir, photo_filename)
768
- photo.save(temp_path)
769
- api = HfApi()
770
- api.upload_file(
771
- path_or_fileobj=temp_path,
772
- path_in_repo=f"photos/{photo_filename}",
773
- repo_id=REPO_ID,
774
- repo_type="dataset",
775
- token=HF_TOKEN_WRITE,
776
- commit_message=f"Обновлено фото для товара {name}"
777
- )
778
- new_photos_list.append(photo_filename)
779
- if os.path.exists(temp_path):
780
- os.remove(temp_path)
781
- products[index]['photos'] = new_photos_list
782
-
783
- products[index]['name'] = name
784
- products[index]['price'] = float(price.replace(',', '.'))
785
- products[index]['description'] = description
786
- products[index]['category'] = category if category in categories else 'Без категории'
787
- products[index]['colors'] = colors if colors else []
788
- save_data(data)
789
- return redirect(url_for('admin'))
790
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
791
  elif action == 'delete':
792
- index = int(request.form.get('index'))
793
- del products[index]
794
- save_data(data)
795
- return redirect(url_for('admin'))
796
-
 
 
 
 
 
 
 
 
 
 
 
 
797
  admin_html = '''
798
  <!DOCTYPE html>
799
  <html lang="ru">
@@ -821,29 +1059,27 @@ def admin():
821
  max-width: 1200px;
822
  margin: 0 auto;
823
  }
824
- .header {
825
  display: flex;
826
  align-items: center;
827
  padding: 15px 0;
828
  border-bottom: 1px solid var(--primary-color);
 
829
  }
830
  .header-logo {
831
- width: 60px;
832
- height: 60px;
833
  border-radius: 50%;
834
  object-fit: cover;
835
- box-shadow: 0 4px 15px var(--shadow-color);
836
- transition: transform 0.3s ease, box-shadow 0.3s ease;
837
  margin-right: 15px;
838
  }
839
- .header-logo:hover {
840
- transform: scale(1.1);
841
- box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
842
- }
843
  h1, h2 {
844
  font-weight: 600;
845
  margin-bottom: 20px;
846
  color: var(--primary-color);
 
 
847
  }
848
  form {
849
  background: var(--light-text);
@@ -856,23 +1092,24 @@ def admin():
856
  font-weight: 500;
857
  margin-top: 15px;
858
  display: block;
 
859
  }
860
  input, textarea, select {
861
  width: 100%;
862
- padding: 12px;
863
- margin-top: 5px;
864
  border: 1px solid var(--secondary-color);
865
  border-radius: 8px;
866
  font-size: 1rem;
867
  transition: all 0.3s ease;
 
868
  }
869
  input:focus, textarea:focus, select:focus {
870
  border-color: var(--primary-color);
871
  box-shadow: 0 0 5px rgba(244, 143, 177, 0.3);
872
  outline: none;
873
  }
874
- button {
875
- padding: 12px 20px;
876
  border: none;
877
  border-radius: 8px;
878
  background-color: var(--primary-color);
@@ -881,6 +1118,8 @@ def admin():
881
  cursor: pointer;
882
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
883
  margin-top: 15px;
 
 
884
  }
885
  button:hover {
886
  background-color: #E91E63;
@@ -897,6 +1136,7 @@ def admin():
897
  .product-list, .category-list {
898
  display: grid;
899
  gap: 20px;
 
900
  }
901
  .product-item, .category-item {
902
  background: var(--light-text);
@@ -904,23 +1144,91 @@ def admin():
904
  border-radius: 15px;
905
  box-shadow: 0 4px 15px var(--shadow-color);
906
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
907
  .edit-form {
908
- margin-top: 15px;
909
  padding: 15px;
910
  background: #f7fafc;
911
  border-radius: 10px;
 
912
  }
913
  .color-input-group {
914
  display: flex;
915
  gap: 10px;
916
  margin-top: 5px;
917
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
918
  .add-color-btn {
919
  background-color: var(--secondary-color);
920
  }
921
  .add-color-btn:hover {
922
  background-color: #BA68C8;
923
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
924
  </style>
925
  </head>
926
  <body>
@@ -929,39 +1237,46 @@ def admin():
929
  <img src="''' + LOGO_URL + '''" alt="Logo" class="header-logo">
930
  <h1>Админ-панель</h1>
931
  </div>
 
932
  <h1>Добавление товара</h1>
933
  <form method="POST" enctype="multipart/form-data">
934
  <input type="hidden" name="action" value="add">
935
- <label>Название товара:</label>
936
- <input type="text" name="name" required>
937
- <label>Цена:</label>
938
- <input type="number" name="price" step="0.01" required>
939
- <label>Описание:</label>
940
- <textarea name="description" rows="4" required></textarea>
941
- <label>Категория:</label>
942
- <select name="category">
 
 
 
943
  <option value="Без категории">Без категории</option>
944
  {% for category in categories %}
945
- <option value="{{ category }}">{{ category }}</option>
946
  {% endfor %}
947
  </select>
948
- <label>Фотографии (до 10):</label>
949
- <input type="file" name="photos" accept="image/*" multiple>
 
 
950
  <label>Цвета:</label>
951
- <div id="color-inputs">
952
  <div class="color-input-group">
953
  <input type="text" name="colors" placeholder="Например: Красный">
954
  </div>
955
  </div>
956
- <button type="button" class="add-color-btn" onclick="addColorInput()">Добавить цвет</button>
 
957
  <button type="submit">Добавить товар</button>
958
  </form>
959
 
960
  <h1>Управление категориями</h1>
961
  <form method="POST">
962
  <input type="hidden" name="action" value="add_category">
963
- <label>Название категории:</label>
964
- <input type="text" name="category_name" required>
965
  <button type="submit">Добавить</button>
966
  </form>
967
 
@@ -969,39 +1284,40 @@ def admin():
969
  <div class="category-list">
970
  {% for category in categories %}
971
  <div class="category-item">
972
- <h3>{{ category }}</h3>
973
  <form method="POST" style="display: inline;">
974
  <input type="hidden" name="action" value="delete_category">
975
- <input type="hidden" name="category_index" value="{{ loop.index0 }}">
976
- <button type="submit" class="delete-button">Удалить</button>
977
  </form>
978
  </div>
979
  {% endfor %}
980
  </div>
981
 
982
  <h2>Управление базой данных</h2>
983
- <form method="POST" action="{{ url_for('backup') }}" style="display: inline;">
984
- <button type="submit">Создать копию</button>
985
- </form>
986
- <form method="GET" action="{{ url_for('download') }}" style="display: inline;">
987
- <button type="submit">Скачать базу</button>
988
- </form>
 
989
 
990
- <h2>Список товаров</h2>
991
  <div class="product-list">
992
  {% for product in products %}
993
  <div class="product-item">
994
- <h3>{{ product['name'] }}</h3>
995
- <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
996
  <p><strong>Цена:</strong> {{ product['price'] }} с</p>
997
- <p><strong>Описание:</strong> {{ product['description'] }}</p>
998
- <p><strong>Цвета:</strong> {{ product.get('colors', ['Нет цветов'])|join(', ') }}</p>
999
  {% if product.get('photos') and product['photos']|length > 0 %}
1000
- <div style="display: flex; flex-wrap: wrap; gap: 10px;">
1001
  {% for photo in product['photos'] %}
1002
- <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo }}"
1003
- alt="{{ product['name'] }}"
1004
- style="max-width: 100px; border-radius: 10px;">
1005
  {% endfor %}
1006
  </div>
1007
  {% endif %}
@@ -1010,71 +1326,101 @@ def admin():
1010
  <form method="POST" enctype="multipart/form-data" class="edit-form">
1011
  <input type="hidden" name="action" value="edit">
1012
  <input type="hidden" name="index" value="{{ loop.index0 }}">
1013
- <label>Название:</label>
1014
- <input type="text" name="name" value="{{ product['name'] }}" required>
1015
- <label>Цена:</label>
1016
- <input type="number" name="price" step="0.01" value="{{ product['price'] }}" required>
1017
- <label>Описание:</label>
1018
- <textarea name="description" rows="4" required>{{ product['description'] }}</textarea>
1019
- <label>Категория:</label>
1020
- <select name="category">
 
 
 
1021
  <option value="Без категории" {% if product.get('category', 'Без категории') == 'Без категории' %}selected{% endif %}>Без категории</option>
1022
  {% for category in categories %}
1023
- <option value="{{ category }}" {% if product.get('category') == category %}selected{% endif %}>{{ category }}</option>
1024
  {% endfor %}
1025
  </select>
1026
- <label>Фотографии (до 10):</label>
1027
- <input type="file" name="photos" accept="image/*" multiple>
1028
- <label>Цвета:</label>
1029
- <div id="edit-color-inputs-{{ loop.index0 }}">
1030
- {% for color in product.get('colors', []) %}
1031
- <div class="color-input-group">
1032
- <input type="text" name="colors" value="{{ color }}">
1033
- </div>
1034
- {% endfor %}
1035
- </div>
1036
- <button type="button" class="add-color-btn" onclick="addColorInput('edit-color-inputs-{{ loop.index0 }}')">Добавить цвет</button>
1037
- <button type="submit">Сохранить</button>
 
 
 
 
 
1038
  </form>
1039
  </details>
1040
- <form method="POST">
1041
  <input type="hidden" name="action" value="delete">
1042
  <input type="hidden" name="index" value="{{ loop.index0 }}">
1043
- <button type="submit" class="delete-button">Удалить</button>
1044
  </form>
1045
  </div>
1046
  {% endfor %}
1047
  </div>
1048
  </div>
1049
  <script>
1050
- function addColorInput(containerId = 'color-inputs') {
1051
  const container = document.getElementById(containerId);
1052
- const newInput = document.createElement('div');
1053
- newInput.className = 'color-input-group';
1054
- newInput.innerHTML = '<input type="text" name="colors" placeholder="Например: Красный">';
1055
- container.appendChild(newInput);
 
 
 
 
 
 
 
1056
  }
1057
  </script>
1058
  </body>
1059
  </html>
1060
  '''
1061
- return render_template_string(admin_html, products=products, categories=categories, repo_id=REPO_ID)
1062
 
1063
  @app.route('/backup', methods=['POST'])
1064
  def backup():
1065
- upload_db_to_hf()
1066
- return "Резервная копия создана.", 200
 
 
 
1067
 
1068
  @app.route('/download', methods=['GET'])
1069
  def download():
1070
- download_db_from_hf()
1071
- return "База данных скачана.", 200
 
 
 
1072
 
1073
  if __name__ == '__main__':
 
 
 
 
1074
  backup_thread = threading.Thread(target=periodic_backup, daemon=True)
1075
  backup_thread.start()
 
 
1076
  try:
1077
  load_data()
1078
  except Exception as e:
1079
- logging.error(f"Не удалось загрузить базу данных: {e}")
 
 
 
1080
  app.run(debug=True, host='0.0.0.0', port=7860)
 
1
+ from flask import Flask, render_template_string, request, redirect, url_for, send_from_directory
2
  import json
3
  import os
4
  import logging
 
6
  import time
7
  from datetime import datetime
8
  from huggingface_hub import HfApi, hf_hub_download
9
+ from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError
10
  from werkzeug.utils import secure_filename
11
+ import urllib.parse
12
 
13
  app = Flask(__name__)
14
  DATA_FILE = 'dataasdem.json'
15
+ PHOTOS_DIR = 'photos'
16
 
17
  REPO_ID = "flpolprojects/dataasdem"
18
  HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
 
29
  data = json.load(file)
30
  logging.info("Данные успешно загружены из JSON")
31
  if not isinstance(data, dict) or 'products' not in data or 'categories' not in data:
32
+ # Attempt to handle older formats gracefully if possible, or default
33
+ if isinstance(data, list):
34
+ # Assume it was just a list of products in older format
35
+ return {'products': data, 'categories': []}
36
+ else:
37
+ # Fallback for unexpected format
38
+ logging.warning("Неожиданный формат данных в JSON.")
39
+ return {'products': [], 'categories': []}
40
  return data
41
  except FileNotFoundError:
42
+ logging.warning(f"Локальный файл базы данных ({DATA_FILE}) не найден после скачивания.")
43
  return {'products': [], 'categories': []}
44
  except json.JSONDecodeError:
45
  logging.error("Ошибка: Невозможно декодировать JSON файл.")
46
  return {'products': [], 'categories': []}
47
  except RepositoryNotFoundError:
48
+ logging.error(f"Репозиторий '{REPO_ID}' не найден на Hugging Face. Создание локальной базы данных.")
49
  return {'products': [], 'categories': []}
50
+ except HfHubHTTPError as e:
51
+ logging.error(f"Ошибка HTTP при скачивании из Hugging Face: {e}")
52
+ logging.warning(f"Попытка загрузить локальный файл '{DATA_FILE}' если существует.")
53
+ try:
54
+ with open(DATA_FILE, 'r', encoding='utf-8') as file:
55
+ data = json.load(file)
56
+ logging.info("Данные успешно загружены из локального JSON после ошибки HF.")
57
+ if not isinstance(data, dict) or 'products' not in data or 'categories' not in data:
58
+ if isinstance(data, list):
59
+ return {'products': data, 'categories': []}
60
+ else:
61
+ logging.warning("Неожиданный формат данных в локальном JSON.")
62
+ return {'products': [], 'categories': []}
63
+ return data
64
+ except FileNotFoundError:
65
+ logging.warning(f"Локальный файл '{DATA_FILE}' также не найден.")
66
+ return {'products': [], 'categories': []}
67
+ except json.JSONDecodeError:
68
+ logging.error("Ошибка: Невозможно декодировать локальный JSON файл.")
69
+ return {'products': [], 'categories': []}
70
  except Exception as e:
71
+ logging.error(f"Произошла неизвестная ошибка при загрузке данных: {e}")
72
  return {'products': [], 'categories': []}
73
 
74
  def save_data(data):
 
82
  raise
83
 
84
  def upload_db_to_hf():
85
+ if not HF_TOKEN_WRITE:
86
+ logging.warning("HF_TOKEN не установлен. Пропуск загрузки на Hugging Face.")
87
+ return
88
  try:
89
  api = HfApi()
90
  api.upload_file(
 
97
  )
98
  logging.info("Резервная копия JSON базы успешно загружена на Hugging Face.")
99
  except Exception as e:
100
+ logging.error(f"Ошибка при загрузке резервной копии на Hugging Face: {e}")
101
 
102
  def download_db_from_hf():
103
+ if not HF_TOKEN_READ:
104
+ logging.warning("HF_TOKEN_READ не установлен. Пропус�� скачивания из Hugging Face.")
105
+ return
106
  try:
107
  hf_hub_download(
108
  repo_id=REPO_ID,
 
113
  local_dir_use_symlinks=False
114
  )
115
  logging.info("JSON база успешно скачана из Hugging Face.")
116
+ except RepositoryNotFoundError:
117
+ logging.warning(f"Репозиторий '{REPO_ID}' не найден. Скачивание невозможно.")
118
+ raise # Re-raise to be caught by load_data
119
  except Exception as e:
120
+ logging.error(f"Ошибка при скачивании JSON базы из Hugging Face: {e}")
121
+ raise # Re-raise to be caught by load_data
122
 
123
  def periodic_backup():
124
  while True:
125
  upload_db_to_hf()
126
  time.sleep(800)
127
 
128
+ catalog_html = '''
129
+ <!DOCTYPE html>
130
+ <html lang="ru">
131
+ <head>
132
+ <meta charset="UTF-8">
133
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
134
+ <title>Asdem - нижнее белье оптом </title>
135
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
136
+ <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
137
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.css">
138
+ <style>
139
+ :root {
140
+ --primary-color: #F48FB1;
141
+ --secondary-color: #CE93D8;
142
+ --text-color: #333;
143
+ --bg-color: #FCE4EC;
144
+ --light-text: #fff;
145
+ --shadow-color: rgba(0, 0, 0, 0.1);
146
+ }
147
+ * {
148
+ margin: 0;
149
+ padding: 0;
150
+ box-sizing: border-box;
151
+ }
152
+ body {
153
+ font-family: 'Poppins', sans-serif;
154
+ background-color: var(--bg-color);
155
+ color: var(--text-color);
156
+ line-height: 1.6;
157
+ transition: background-color 0.3s, color 0.3s;
158
+ }
159
+ .container {
160
+ max-width: 1300px;
161
+ margin: 0 auto;
162
+ padding: 20px;
163
+ }
164
+ .header {
165
+ display: flex;
166
+ justify-content: space-between;
167
+ align-items: center;
168
+ padding: 15px 0;
169
+ border-bottom: 1px solid var(--primary-color);
170
+ }
171
+ .header-logo {
172
+ width: 60px;
173
+ height: 60px;
174
+ border-radius: 50%;
175
+ object-fit: cover;
176
+ box-shadow: 0 4px 15px var(--shadow-color);
177
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
178
+ }
179
+ .header-logo:hover {
180
+ transform: scale(1.1);
181
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
182
+ }
183
+ .header h1 {
184
+ font-size: 1.5rem;
185
+ font-weight: 600;
186
+ margin-left: 15px;
187
+ color: var(--primary-color);
188
+ }
189
+ .filters-container {
190
+ margin: 20px 0;
191
+ display: flex;
192
+ flex-wrap: wrap;
193
+ gap: 10px;
194
+ justify-content: center;
195
+ }
196
+ .search-container {
197
+ margin: 20px 0;
198
+ text-align: center;
199
+ }
200
+ #search-input {
201
+ width: 90%;
202
+ max-width: 600px;
203
+ padding: 12px 18px;
204
+ font-size: 1rem;
205
+ border: 1px solid var(--secondary-color);
206
+ border-radius: 8px;
207
+ outline: none;
208
+ box-shadow: 0 2px 5px var(--shadow-color);
209
+ transition: all 0.3s ease;
210
+ }
211
+ #search-input:focus {
212
+ border-color: var(--primary-color);
213
+ box-shadow: 0 4px 15px rgba(244, 143, 177, 0.3);
214
+ }
215
+ .category-filter {
216
+ padding: 8px 16px;
217
+ border: 1px solid var(--secondary-color);
218
+ border-radius: 8px;
219
+ background-color: var(--light-text);
220
+ color: var(--text-color);
221
+ cursor: pointer;
222
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
223
+ font-size: 0.9rem;
224
+ font-weight: 400;
225
+ }
226
+ .category-filter.active, .category-filter:hover {
227
+ background-color: var(--primary-color);
228
+ color: var(--light-text);
229
+ border-color: var(--primary-color);
230
+ box-shadow: 0 2px 10px rgba(244, 143, 177, 0.4);
231
+ }
232
+ .products-grid {
233
+ display: grid;
234
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
235
+ gap: 15px;
236
+ padding: 10px;
237
+ }
238
+ .product {
239
+ background: var(--light-text);
240
+ border-radius: 15px;
241
+ padding: 15px;
242
+ box-shadow: 0 4px 15px var(--shadow-color);
243
+ transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.3s ease;
244
+ overflow: hidden;
245
+ display: flex;
246
+ flex-direction: column;
247
+ }
248
+ .product:hover {
249
+ transform: translateY(-5px) scale(1.02);
250
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
251
+ }
252
+ .product-image {
253
+ width: 100%;
254
+ aspect-ratio: 1;
255
+ background-color: #fff;
256
+ border-radius: 10px;
257
+ overflow: hidden;
258
+ display: flex;
259
+ justify-content: center;
260
+ align-items: center;
261
+ }
262
+ .product-image img {
263
+ max-width: 100%;
264
+ max-height: 100%;
265
+ object-fit: contain;
266
+ transition: transform 0.3s ease;
267
+ }
268
+ .product-image img:hover {
269
+ transform: scale(1.1);
270
+ }
271
+ .product h2 {
272
+ font-size: 1rem;
273
+ font-weight: 600;
274
+ margin: 10px 0 5px 0;
275
+ text-align: center;
276
+ white-space: nowrap;
277
+ overflow: hidden;
278
+ text-overflow: ellipsis;
279
+ }
280
+ .product-price {
281
+ font-size: 1.1rem;
282
+ color: #ef4444;
283
+ font-weight: 700;
284
+ text-align: center;
285
+ margin: 5px 0;
286
+ }
287
+ .product-description {
288
+ font-size: 0.8rem;
289
+ color: #718096;
290
+ text-align: center;
291
+ margin-bottom: 15px;
292
+ overflow: hidden;
293
+ text-overflow: ellipsis;
294
+ display: -webkit-box;
295
+ -webkit-line-clamp: 2;
296
+ -webkit-box-orient: vertical;
297
+ flex-grow: 1;
298
+ }
299
+ .product-buttons {
300
+ margin-top: auto; /* Push buttons to the bottom */
301
+ }
302
+ .product-button {
303
+ display: block;
304
+ width: 100%;
305
+ padding: 8px;
306
+ border: none;
307
+ border-radius: 8px;
308
+ background-color: var(--primary-color);
309
+ color: var(--light-text);
310
+ font-size: 0.8rem;
311
+ font-weight: 500;
312
+ cursor: pointer;
313
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
314
+ margin: 5px 0;
315
+ text-align: center;
316
+ text-decoration: none;
317
+ }
318
+ .product-button:hover {
319
+ background-color: #E91E63;
320
+ box-shadow: 0 4px 15px rgba(233, 30, 99, 0.4);
321
+ transform: translateY(-2px);
322
+ }
323
+ .add-to-cart {
324
+ background-color: var(--secondary-color);
325
+ }
326
+ .add-to-cart:hover {
327
+ background-color: #BA68C8;
328
+ box-shadow: 0 4px 15px rgba(186, 104, 200, 0.4);
329
+ }
330
+ #cart-button {
331
+ position: fixed;
332
+ bottom: 20px;
333
+ right: 20px;
334
+ background-color: #ef4444;
335
+ color: var(--light-text);
336
+ border: none;
337
+ border-radius: 50%;
338
+ width: 50px;
339
+ height: 50px;
340
+ font-size: 1.2rem;
341
+ cursor: pointer;
342
+ display: none;
343
+ box-shadow: 0 4px 15px rgba(239, 68, 68, 0.4);
344
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
345
+ z-index: 1000;
346
+ }
347
+ .modal {
348
+ display: none;
349
+ position: fixed;
350
+ z-index: 1001;
351
+ left: 0;
352
+ top: 0;
353
+ width: 100%;
354
+ height: 100%;
355
+ background-color: rgba(0,0,0,0.5);
356
+ backdrop-filter: blur(5px);
357
+ overflow: auto; /* Add scroll for smaller screens */
358
+ }
359
+ .modal-content {
360
+ background: var(--light-text);
361
+ margin: 5% auto;
362
+ padding: 20px;
363
+ border-radius: 15px;
364
+ width: 90%;
365
+ max-width: 700px;
366
+ box-shadow: 0 10px 30px rgba(0,0,0,0.2);
367
+ animation: slideIn 0.3s ease-out;
368
+ }
369
+ @keyframes slideIn {
370
+ from { transform: translateY(-50px); opacity: 0; }
371
+ to { transform: translateY(0); opacity: 1; }
372
+ }
373
+ .close {
374
+ float: right;
375
+ font-size: 1.5rem;
376
+ color: #718096;
377
+ cursor: pointer;
378
+ transition: color 0.3s;
379
+ }
380
+ .close:hover {
381
+ color: var(--primary-color);
382
+ }
383
+ .cart-item {
384
+ display: flex;
385
+ justify-content: space-between;
386
+ align-items: center;
387
+ padding: 15px 0;
388
+ border-bottom: 1px solid var(--secondary-color);
389
+ }
390
+ .cart-item:last-child {
391
+ border-bottom: none;
392
+ }
393
+ .cart-item .item-info {
394
+ display: flex;
395
+ align-items: center;
396
+ flex-grow: 1;
397
+ margin-right: 15px;
398
+ }
399
+ .cart-item img {
400
+ width: 50px;
401
+ height: 50px;
402
+ object-fit: contain;
403
+ border-radius: 8px;
404
+ margin-right: 15px;
405
+ flex-shrink: 0;
406
+ }
407
+ .cart-item .item-details {
408
+ flex-grow: 1;
409
+ }
410
+ .cart-item .item-details strong {
411
+ display: block;
412
+ font-size: 1rem;
413
+ white-space: nowrap;
414
+ overflow: hidden;
415
+ text-overflow: ellipsis;
416
+ }
417
+ .cart-item .item-details p {
418
+ font-size: 0.9rem;
419
+ color: #718096;
420
+ }
421
+ .cart-item span {
422
+ font-size: 1rem;
423
+ font-weight: 600;
424
+ color: #ef4444;
425
+ flex-shrink: 0;
426
+ }
427
+
428
+ .quantity-input, .color-select {
429
+ width: 100%;
430
+ max-width: 150px; /* Adjust max-width */
431
+ padding: 8px;
432
+ border: 1px solid var(--secondary-color);
433
+ border-radius: 8px;
434
+ font-size: 1rem;
435
+ margin: 5px 0;
436
+ display: block; /* Make them block elements */
437
+ margin-bottom: 15px;
438
+ }
439
+ .modal-content button {
440
+ margin-top: 0; /* Remove extra margin */
441
+ }
442
+ .modal-content .product-button {
443
+ margin-top: 15px; /* Restore margin for modal buttons */
444
+ }
445
+
446
+ .clear-cart {
447
+ background-color: #ef4444;
448
+ }
449
+ .clear-cart:hover {
450
+ background-color: #dc2626;
451
+ box-shadow: 0 4px 15px rgba(220, 38, 38, 0.4);
452
+ }
453
+ .order-button {
454
+ background-color: var(--secondary-color);
455
+ }
456
+ .order-button:hover {
457
+ background-color: #BA68C8;
458
+ box-shadow: 0 4px 15px rgba(186, 104, 200, 0.4);
459
+ }
460
+ @media (max-width: 768px) {
461
+ .header {
462
+ flex-direction: column;
463
  text-align: center;
464
+ }
465
+ .header-logo {
466
+ margin-right: 0;
467
+ margin-bottom: 10px;
468
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
469
  .products-grid {
470
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
471
+ }
472
+ .modal-content {
473
+ margin: 10% auto;
474
+ }
475
+ .quantity-input, .color-select {
476
+ max-width: none;
477
+ }
478
+ }
479
+ @media (max-width: 480px) {
480
+ .products-grid {
481
+ grid-template-columns: 1fr; /* Stack products on very small screens */
482
+ }
483
+ .modal-content {
484
+ margin: 15% auto;
485
+ }
486
+ }
487
+ </style>
488
+ </head>
489
+ <body>
490
+ <div class="container">
491
+ <div class="header">
492
+ <img src="''' + LOGO_URL + '''" alt="Logo" class="header-logo">
493
+ <h1>Каталог</h1>
494
+ </div>
495
+ <div class="filters-container">
496
+ <button class="category-filter active" data-category="all">Все категории</button>
497
+ {% for category in categories %}
498
+ <button class="category-filter" data-category="{{ category }}">{{ category }}</button>
499
+ {% endfor %}
500
+ </div>
501
+ <div class="search-container">
502
+ <input type="text" id="search-input" placeholder="Поиск товаров...">
503
+ </div>
504
+ <div class="products-grid" id="products-grid">
505
+ {% for product in products %}
506
+ <div class="product"
507
+ data-name="{{ product['name']|lower|e }}"
508
+ data-description="{{ product['description']|lower|e }}"
509
+ data-category="{{ product.get('category', 'Без категории')|e }}">
510
+ {% if product.get('photos') and product['photos']|length > 0 %}
511
+ <div class="product-image">
512
+ <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product['photos'][0]|e }}"
513
+ alt="{{ product['name']|e }}"
514
+ loading="lazy">
515
+ </div>
516
+ {% endif %}
517
+ <h2>{{ product['name']|e }}</h2>
518
+ <div class="product-price">{{ product['price'] }} с</div>
519
+ <p class="product-description">{{ product['description'][:100] }}{% if product['description']|length > 100 %}...{% endif %}</p>
520
+ <div class="product-buttons">
521
+ <button class="product-button" onclick="openProductModal('{{ product['name']|e }}')">Подробнее</button>
522
+ <button class="product-button add-to-cart" onclick="openQuantityModal('{{ product['name']|e }}')">В корзину</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
523
  </div>
 
524
  </div>
525
+ {% endfor %}
526
  </div>
527
+ </div>
528
 
529
+ <div id="productModal" class="modal">
530
+ <div class="modal-content">
531
+ <span class="close" onclick="closeModal('productModal')">×</span>
532
+ <div id="modalContent"></div>
 
533
  </div>
534
+ </div>
535
 
536
+ <div id="quantityModal" class="modal">
537
+ <div class="modal-content">
538
+ <span class="close" onclick="closeModal('quantityModal')">×</span>
539
+ <h2>Укажите количество и цвет</h2>
540
+ <label for="quantityInput">Количество:</label>
541
+ <input type="number" id="quantityInput" class="quantity-input" min="1" value="1">
542
+ <label for="colorSelect">Цвет:</label>
543
+ <select id="colorSelect" class="color-select"></select>
544
+ <button class="product-button" onclick="confirmAddToCart()">Добавить</button>
545
  </div>
546
+ </div>
547
 
548
+ <div id="cartModal" class="modal">
549
+ <div class="modal-content">
550
+ <span class="close" onclick="closeModal('cartModal')">×</span>
551
+ <h2>Корзина</h2>
552
+ <div id="cartContent"></div>
553
+ <div style="margin-top: 20px; text-align: right;">
554
+ <strong>Итого: <span id="cartTotal">0</span> с</strong>
555
+ <button class="product-button clear-cart" onclick="clearCart()">Очистить</button>
556
+ <button class="product-button order-button" onclick="orderViaWhatsApp()">Заказать</button>
 
557
  </div>
558
  </div>
559
+ </div>
560
 
561
+ <button id="cart-button" onclick="openCartModal()">🛒<span id="cart-count" style="position: absolute; top: -5px; right: -5px; background-color: red; color: white; border-radius: 50%; padding: 2px 5px; font-size: 0.7em; display: none;">0</span></button>
562
 
563
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.js"></script>
564
+ <script>
565
+ // products data is available as a global variable from the template
566
+ const allProducts = {{ products|tojson }};
567
+ let selectedProductName = null;
 
568
 
569
+ function openProductModal(productName) {
570
+ loadProductDetails(productName);
571
+ document.getElementById('productModal').style.display = "block";
572
+ }
573
 
574
+ function closeModal(modalId) {
575
+ document.getElementById(modalId).style.display = "none";
576
+ }
577
 
578
+ function loadProductDetails(productName) {
579
+ const encodedProductName = encodeURIComponent(productName);
580
+ fetch('/product_by_name/' + encodedProductName)
581
+ .then(response => response.text())
582
+ .then(data => {
583
+ document.getElementById('modalContent').innerHTML = data;
584
+ initializeSwiper();
585
+ })
586
+ .catch(error => console.error('Ошибка загрузки деталей продукта:', error));
587
+ }
588
 
589
+ function initializeSwiper() {
590
+ if (document.querySelector('.swiper-container')) {
591
  new Swiper('.swiper-container', {
592
  slidesPerView: 1,
593
  spaceBetween: 20,
594
+ loop: true, // Enable loop if you have multiple images
595
  grabCursor: true,
596
  pagination: { el: '.swiper-pagination', clickable: true },
597
  navigation: { nextEl: '.swiper-button-next', prevEl: '.swiper-button-prev' },
598
+ zoom: { maxRatio: 3 },
599
+ keyboard: { enabled: true },
600
+ mousewheel: { enabled: true },
601
  });
602
+ } else {
603
+ console.warn("Swiper container not found. Swiper not initialized.");
604
+ }
605
+ }
606
+
607
+ function openQuantityModal(productName) {
608
+ selectedProductName = productName;
609
+ const product = allProducts.find(p => p.name === productName);
610
+ if (!product) {
611
+ alert("Продукт не найден!");
612
+ return;
613
  }
614
 
615
+ const colorSelect = document.getElementById('colorSelect');
616
+ colorSelect.innerHTML = '';
617
+ if (product.colors && product.colors.length > 0) {
618
+ product.colors.forEach(color => {
 
 
 
 
 
 
 
 
 
619
  const option = document.createElement('option');
620
+ option.value = color;
621
+ option.text = color;
622
  colorSelect.appendChild(option);
623
+ });
624
+ } else {
625
+ const option = document.createElement('option');
626
+ option.value = 'Без цвета';
627
+ option.text = 'Без цвета';
628
+ colorSelect.appendChild(option);
629
+ }
630
+ document.getElementById('quantityModal').style.display = 'block';
631
+ document.getElementById('quantityInput').value = 1;
632
+ }
633
+
634
+ function confirmAddToCart() {
635
+ if (selectedProductName === null) return;
636
+ const quantityInput = document.getElementById('quantityInput');
637
+ const quantity = parseInt(quantityInput.value) || 1;
638
+ const color = document.getElementById('colorSelect').value;
639
+
640
+ if (quantity <= 0) {
641
+ alert("Укажите количество больше 0");
642
+ quantityInput.value = 1; // Reset to 1
643
+ return;
644
  }
645
 
646
+ let cart = JSON.parse(localStorage.getItem('cart') || '[]');
647
+ const product = allProducts.find(p => p.name === selectedProductName);
648
+
649
+ if (!product) {
650
+ alert("Ошибка: Товар не найден.");
651
+ return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
652
  }
653
 
654
+ const cartItemId = `${product.name}-${color}`;
655
+ const existingItem = cart.find(item => item.id === cartItemId);
656
+
657
+ if (existingItem) {
658
+ existingItem.quantity += quantity;
659
+ } else {
660
+ cart.push({
661
+ id: cartItemId,
662
+ name: product.name,
663
+ price: product.price,
664
+ photo: product.photos && product.photos.length > 0 ? product.photos[0] : '',
665
+ quantity: quantity,
666
+ color: color
667
+ });
668
  }
669
 
670
+ localStorage.setItem('cart', JSON.stringify(cart));
671
+ closeModal('quantityModal');
672
+ updateCartButton();
673
+ alert(`Товар "${product.name}" добавлен в корзину!`);
674
+ }
675
+
676
+ function updateCartButton() {
677
+ const cart = JSON.parse(localStorage.getItem('cart') || '[]');
678
+ const cartButton = document.getElementById('cart-button');
679
+ const cartCount = document.getElementById('cart-count');
680
+ const totalItems = cart.reduce((sum, item) => sum + item.quantity, 0);
681
+
682
+ if (cart.length > 0) {
683
+ cartButton.style.display = 'block';
684
+ cartCount.textContent = totalItems;
685
+ cartCount.style.display = 'block';
686
+ } else {
687
+ cartButton.style.display = 'none';
688
+ cartCount.style.display = 'none';
689
+ }
690
+ }
691
 
692
+ function openCartModal() {
693
+ const cart = JSON.parse(localStorage.getItem('cart') || '[]');
694
+ const cartContent = document.getElementById('cartContent');
695
+ let total = 0;
696
+
697
+ if (cart.length === 0) {
698
+ cartContent.innerHTML = '<p>Корзина пуста</p>';
699
+ } else {
700
+ cartContent.innerHTML = cart.map(item => {
701
+ const itemTotal = parseFloat(item.price) * item.quantity;
702
  total += itemTotal;
703
+ const photoUrl = item.photo ? `https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/${item.photo}` : '';
704
  return `
705
  <div class="cart-item">
706
+ <div class="item-info">
707
+ ${photoUrl ? `<img src="${photoUrl}" alt="${item.name}">` : ''}
708
+ <div class="item-details">
709
  <strong>${item.name}</strong>
710
  <p>${item.price} с × ${item.quantity} (Цвет: ${item.color})</p>
711
  </div>
 
714
  </div>
715
  `;
716
  }).join('');
 
 
 
717
  }
718
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
719
 
720
+ document.getElementById('cartTotal').textContent = total;
721
+ document.getElementById('cartModal').style.display = 'block';
722
+ }
723
+
724
+ function orderViaWhatsApp() {
725
+ const cart = JSON.parse(localStorage.getItem('cart') || '[]');
726
+ if (cart.length === 0) {
727
+ alert("Корзина пуста!");
728
+ return;
729
+ }
730
+ let total = 0;
731
+ let orderText = "Здравствуйте! Хочу сделать заказ:%0A";
732
+ cart.forEach((item, index) => {
733
+ const itemTotal = parseFloat(item.price) * item.quantity;
734
+ total += itemTotal;
735
+ orderText += `${index + 1}. ${item.name} - ${item.price} с × ${item.quantity} шт. (Цвет: ${item.color})%0A`;
736
+ });
737
+ orderText += `Итого к оплате: ${total} с`;
738
+
739
+ const phoneNumber = '996772179559'; // Замените на ваш номер телефона в международном формате
740
+ const whatsappUrl = `https://api.whatsapp.com/send?phone=${phoneNumber}&text=${encodeURIComponent(orderText)}`;
741
+
742
+ window.open(whatsappUrl, '_blank');
743
+ // Optionally clear cart after order attempt
744
+ // clearCart();
745
+ }
746
+
747
+ function clearCart() {
748
+ if (confirm("Вы уверены, что хотите очистить корзину?")) {
749
  localStorage.removeItem('cart');
750
  closeModal('cartModal');
751
  updateCartButton();
752
  }
753
+ }
754
 
755
+ window.onclick = function(event) {
756
+ if (event.target.classList && event.target.classList.contains('modal')) {
757
+ event.target.style.display = "none";
758
  }
759
+ }
760
 
761
+ document.getElementById('search-input').addEventListener('input', filterProducts);
762
+ document.querySelectorAll('.category-filter').forEach(filter => {
763
+ filter.addEventListener('click', function() {
764
+ document.querySelectorAll('.category-filter').forEach(f => f.classList.remove('active'));
765
+ this.classList.add('active');
766
+ filterProducts();
 
767
  });
768
+ });
769
 
770
+ function filterProducts() {
771
+ const searchTerm = document.getElementById('search-input').value.toLowerCase();
772
+ const activeCategory = document.querySelector('.category-filter.active').dataset.category;
773
+ document.querySelectorAll('.product').forEach(productDiv => {
774
+ const name = productDiv.getAttribute('data-name');
775
+ const description = productDiv.getAttribute('data-description');
776
+ const category = productDiv.getAttribute('data-category');
777
+ const matchesSearch = name.includes(searchTerm) || description.includes(searchTerm);
778
+ const matchesCategory = activeCategory === 'all' || category === activeCategory;
779
+ productDiv.style.display = matchesSearch && matchesCategory ? 'flex' : 'none'; // Use flex because product uses flex-direction column
780
+ });
781
+ }
782
 
783
+ // Initial load
784
+ updateCartButton();
785
+ filterProducts(); // Apply initial filter (all categories)
 
 
 
786
 
787
+ </script>
788
+ </body>
789
+ </html>
790
+ '''
791
+
792
+ detail_html = '''
793
+ <div class="product-detail-content" style="padding: 20px;">
794
+ <h2 style="font-size: 1.5rem; font-weight: 600; margin-bottom: 20px; text-align: center;">{{ product['name']|e }}</h2>
795
+ <div class="swiper-container" style="max-width: 400px; margin: 0 auto 20px;">
796
+ <div class="swiper-wrapper">
797
+ {% if product.get('photos') %}
798
+ {% for photo in product['photos'] %}
799
+ <div class="swiper-slide" style="background-color: #fff; display: flex; justify-content: center; align-items: center; border-radius: 10px;">
800
+ <div class="swiper-zoom-container">
801
+ <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo|e }}"
802
+ alt="{{ product['name']|e }}"
803
+ style="max-width: 100%; max-height: 300px; object-fit: contain; display: block;">
 
 
 
 
 
 
 
 
 
804
  </div>
 
805
  </div>
806
+ {% endfor %}
807
+ {% else %}
808
+ <div class="swiper-slide" style="background-color: #fff; display: flex; justify-content: center; align-items: center; border-radius: 10px;">
809
+ <img src="https://via.placeholder.com/300?text=No+Image" alt="No Image" style="max-width: 100%; max-height: 300px; object-fit: contain;">
810
+ </div>
811
+ {% endif %}
812
  </div>
813
+ {% if product.get('photos') and product['photos']|length > 1 %}
814
+ <div class="swiper-pagination"></div>
815
+ <div class="swiper-button-next"></div>
816
+ <div class="swiper-button-prev"></div>
817
+ {% endif %}
818
  </div>
819
+ <p style="margin-bottom: 10px;"><strong>Категория:</strong> {{ product.get('category', 'Без категории')|e }}</p>
820
+ <p style="font-size: 1.2rem; font-weight: 700; color: #ef4444; margin-bottom: 15px;"><strong>Цена:</strong> {{ product['price'] }} с</p>
821
+ <p style="margin-bottom: 15px;"><strong>Описание:</strong> {{ product['description']|e }}</p>
822
+ <p style="margin-bottom: 15px;"><strong>Доступные цвета:</strong> {{ product.get('colors', ['Нет цветов'])|join(', ')|e }}</p>
823
+ <button class="product-button add-to-cart" onclick="closeModal('productModal'); openQuantityModal('{{ product['name']|e }}')" style="display: block; width: 100%; margin-top: 20px;">Добавить в корзину</button>
824
+ </div>
825
+ '''
826
+
827
+
828
+ @app.route('/')
829
+ def catalog():
830
+ data = load_data()
831
+ # Sort products for display in the catalog by added_at, newest first
832
+ products_for_display = sorted(data.get('products', []), key=lambda x: x.get('added_at', ''), reverse=True)
833
+ categories = data.get('categories', [])
834
+
835
+ # Pass the sorted list to the template
836
+ return render_template_string(catalog_html, products=products_for_display, categories=categories, repo_id=REPO_ID, LOGO_URL=LOGO_URL)
837
+
838
+ @app.route('/product_by_name/<string:product_name>')
839
+ def product_detail_by_name(product_name):
840
+ data = load_data()
841
+ # Find the product by name in the *unsorted* list
842
+ product = next((p for p in data.get('products', []) if p['name'] == product_name), None)
843
+
844
+ if product:
845
+ return render_template_string(detail_html, product=product, repo_id=REPO_ID)
846
+ else:
847
+ return "Продукт не найден", 404
848
 
849
  @app.route('/admin', methods=['GET', 'POST'])
850
  def admin():
851
  data = load_data()
852
+ products = data.get('products', [])
853
+ categories = data.get('categories', [])
854
 
855
  if request.method == 'POST':
856
  action = request.form.get('action')
857
+
858
  if action == 'add_category':
859
+ category_name = request.form.get('category_name').strip()
860
  if category_name and category_name not in categories:
861
  categories.append(category_name)
862
  save_data(data)
 
864
  return "Ошибка: Категория уже существует или не указано название", 400
865
 
866
  elif action == 'delete_category':
867
+ try:
868
+ category_name_to_delete = request.form.get('category_name_to_delete')
869
+ if category_name_to_delete in categories:
870
+ categories.remove(category_name_to_delete)
871
+ # Change category for products using the deleted one
872
+ for product in products:
873
+ if product.get('category') == category_name_to_delete:
874
+ product['category'] = 'Без категории'
875
+ save_data(data)
876
+ return redirect(url_for('admin'))
877
+ else:
878
+ return "Ошибка: Категория не найдена.", 400
879
+ except Exception as e:
880
+ logging.error(f"Ошибка при удалении категории: {e}")
881
+ return f"Ошибка при удалении категории: {e}", 500
882
 
883
  elif action == 'add':
884
+ name = request.form.get('name').strip()
885
+ price_str = request.form.get('price').strip()
886
+ description = request.form.get('description').strip()
887
  category = request.form.get('category')
888
  photos_files = request.files.getlist('photos')
889
+ colors = [c.strip() for c in request.form.getlist('colors') if c.strip()]
890
+
891
+ try:
892
+ price = float(price_str.replace(',', '.'))
893
+ except ValueError:
894
+ return "Ошибка: Неверный формат цены", 400
895
+
896
+ if not name or price is None or not description:
897
+ return "Ошибка: Заполните все обязательные поля", 400
898
+
899
  photos_list = []
 
900
  if photos_files:
901
+ for photo in photos_files:
902
  if photo and photo.filename:
903
+ # Ensure filename is secure and unique if needed (e.g., timestamp prefix)
904
+ original_filename = secure_filename(photo.filename)
905
+ # Use a timestamp or UUID to make filename unique in the repo
906
+ timestamp = datetime.now().strftime("%Y%m%d%H%M%S%f")
907
+ photo_filename = f"{timestamp}_{original_filename}"
908
+
909
  uploads_dir = 'uploads'
910
  os.makedirs(uploads_dir, exist_ok=True)
911
  temp_path = os.path.join(uploads_dir, photo_filename)
912
+ try:
913
+ photo.save(temp_path)
914
+ api = HfApi()
915
+ # Upload to a dedicated 'photos' directory in the HF dataset repo
916
+ api.upload_file(
917
+ path_or_fileobj=temp_path,
918
+ path_in_repo=f"photos/{photo_filename}",
919
+ repo_id=REPO_ID,
920
+ repo_type="dataset",
921
+ token=HF_TOKEN_WRITE,
922
+ commit_message=f"Добавлено фото '{original_filename}' для товара '{name}'"
923
+ )
924
+ photos_list.append(photo_filename)
925
+ except Exception as e:
926
+ logging.error(f"Ошибка при загрузке фото {original_filename} на HF: {e}")
927
+ # Decide if you want to stop or continue with other photos
928
+ pass
929
+ finally:
930
+ # Clean up the temporary file
931
+ if os.path.exists(temp_path):
932
+ os.remove(temp_path)
933
+
934
  new_product = {
935
  'name': name,
936
  'price': price,
937
  'description': description,
938
  'category': category if category in categories else 'Без категории',
939
  'photos': photos_list,
940
+ 'colors': colors,
941
  'added_at': datetime.now().isoformat()
942
  }
943
  products.append(new_product)
944
  save_data(data)
945
  return redirect(url_for('admin'))
946
+
947
  elif action == 'edit':
948
+ try:
949
+ index = int(request.form.get('index'))
950
+ if 0 <= index < len(products):
951
+ product_to_edit = products[index]
952
+
953
+ name = request.form.get('name').strip()
954
+ price_str = request.form.get('price').strip()
955
+ description = request.form.get('description').strip()
956
+ category = request.form.get('category')
957
+ photos_files = request.files.getlist('photos')
958
+ colors = [c.strip() for c in request.form.getlist('colors') if c.strip()]
959
+
960
+ try:
961
+ price = float(price_str.replace(',', '.'))
962
+ except ValueError:
963
+ return "Ошибка: Неверный формат цены", 400
964
+
965
+ if not name or price is None or not description:
966
+ return "Ошибка: Заполните все обязательные поля для редактирования", 400
967
+
968
+ # Handle new photos
969
+ if photos_files and any(photo.filename for photo in photos_files):
970
+ new_photos_list = []
971
+ for photo in photos_files:
972
+ if photo and photo.filename:
973
+ original_filename = secure_filename(photo.filename)
974
+ timestamp = datetime.now().strftime("%Y%m%d%H%M%S%f")
975
+ photo_filename = f"{timestamp}_{original_filename}"
976
+ uploads_dir = 'uploads'
977
+ os.makedirs(uploads_dir, exist_ok=True)
978
+ temp_path = os.path.join(uploads_dir, photo_filename)
979
+ try:
980
+ photo.save(temp_path)
981
+ api = HfApi()
982
+ api.upload_file(
983
+ path_or_fileobj=temp_path,
984
+ path_in_repo=f"photos/{photo_filename}",
985
+ repo_id=REPO_ID,
986
+ repo_type="dataset",
987
+ token=HF_TOKEN_WRITE,
988
+ commit_message=f"Обновлено/добавлено фото '{original_filename}' для товара '{name}'"
989
+ )
990
+ new_photos_list.append(photo_filename)
991
+ except Exception as e:
992
+ logging.error(f"Ошибка при загрузке фото {original_filename} на HF во время редактирования: {e}")
993
+ pass
994
+ finally:
995
+ if os.path.exists(temp_path):
996
+ os.remove(temp_path)
997
+ product_to_edit['photos'] = new_photos_list
998
+ # else: no new photos uploaded, keep existing ones
999
+
1000
+ product_to_edit['name'] = name
1001
+ product_to_edit['price'] = price
1002
+ product_to_edit['description'] = description
1003
+ product_to_edit['category'] = category if category in categories else 'Без категории'
1004
+ product_to_edit['colors'] = colors
1005
+
1006
+ save_data(data)
1007
+ return redirect(url_for('admin'))
1008
+ else:
1009
+ return "Ошибка: Товар с указанным индексом не найден.", 404
1010
+ except ValueError:
1011
+ return "Ошибка: Неверный индекс товара.", 400
1012
+ except Exception as e:
1013
+ logging.error(f"Ошибка при редактировании товара: {e}")
1014
+ return f"Ошибка при редактировании товара: {e}", 500
1015
+
1016
+
1017
  elif action == 'delete':
1018
+ try:
1019
+ index = int(request.form.get('index'))
1020
+ if 0 <= index < len(products):
1021
+ # Optional: Add logic here to delete photos from Hugging Face repo
1022
+ # This would involve listing files in the 'photos' directory for this product
1023
+ # and using api.delete_file
1024
+ del products[index]
1025
+ save_data(data)
1026
+ return redirect(url_for('admin'))
1027
+ else:
1028
+ return "Ошибка: Товар с указанным индексом не найден.", 404
1029
+ except ValueError:
1030
+ return "Ошибка: Неверный индекс товара.", 400
1031
+ except Exception as e:
1032
+ logging.error(f"Ошибка при удалении товара: {e}")
1033
+ return f"Ошибка при удалении товара: {e}", 500
1034
+
1035
  admin_html = '''
1036
  <!DOCTYPE html>
1037
  <html lang="ru">
 
1059
  max-width: 1200px;
1060
  margin: 0 auto;
1061
  }
1062
+ .header {
1063
  display: flex;
1064
  align-items: center;
1065
  padding: 15px 0;
1066
  border-bottom: 1px solid var(--primary-color);
1067
+ margin-bottom: 20px;
1068
  }
1069
  .header-logo {
1070
+ width: 50px;
1071
+ height: 50px;
1072
  border-radius: 50%;
1073
  object-fit: cover;
1074
+ box-shadow: 0 4px 10px var(--shadow-color);
 
1075
  margin-right: 15px;
1076
  }
 
 
 
 
1077
  h1, h2 {
1078
  font-weight: 600;
1079
  margin-bottom: 20px;
1080
  color: var(--primary-color);
1081
+ border-bottom: 1px dashed var(--secondary-color);
1082
+ padding-bottom: 10px;
1083
  }
1084
  form {
1085
  background: var(--light-text);
 
1092
  font-weight: 500;
1093
  margin-top: 15px;
1094
  display: block;
1095
+ margin-bottom: 5px;
1096
  }
1097
  input, textarea, select {
1098
  width: 100%;
1099
+ padding: 10px;
 
1100
  border: 1px solid var(--secondary-color);
1101
  border-radius: 8px;
1102
  font-size: 1rem;
1103
  transition: all 0.3s ease;
1104
+ box-sizing: border-box; /* Include padding and border in element's total width */
1105
  }
1106
  input:focus, textarea:focus, select:focus {
1107
  border-color: var(--primary-color);
1108
  box-shadow: 0 0 5px rgba(244, 143, 177, 0.3);
1109
  outline: none;
1110
  }
1111
+ button {
1112
+ padding: 10px 15px;
1113
  border: none;
1114
  border-radius: 8px;
1115
  background-color: var(--primary-color);
 
1118
  cursor: pointer;
1119
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
1120
  margin-top: 15px;
1121
+ display: inline-block;
1122
+ text-decoration: none;
1123
  }
1124
  button:hover {
1125
  background-color: #E91E63;
 
1136
  .product-list, .category-list {
1137
  display: grid;
1138
  gap: 20px;
1139
+ margin-top: 20px;
1140
  }
1141
  .product-item, .category-item {
1142
  background: var(--light-text);
 
1144
  border-radius: 15px;
1145
  box-shadow: 0 4px 15px var(--shadow-color);
1146
  }
1147
+ .category-item {
1148
+ display: flex;
1149
+ justify-content: space-between;
1150
+ align-items: center;
1151
+ flex-wrap: wrap;
1152
+ }
1153
+ .category-item h3 {
1154
+ margin: 0;
1155
+ padding: 0;
1156
+ border-bottom: none;
1157
+ flex-grow: 1;
1158
+ margin-right: 10px;
1159
+ }
1160
+ .category-item form {
1161
+ margin: 0;
1162
+ padding: 0;
1163
+ box-shadow: none;
1164
+ background: none;
1165
+ }
1166
+ .category-item button {
1167
+ margin-top: 0;
1168
+ }
1169
  .edit-form {
1170
+ margin-top: 20px;
1171
  padding: 15px;
1172
  background: #f7fafc;
1173
  border-radius: 10px;
1174
+ box-shadow: inset 0 1px 3px rgba(0,0,0,0.05);
1175
  }
1176
  .color-input-group {
1177
  display: flex;
1178
  gap: 10px;
1179
  margin-top: 5px;
1180
+ align-items: center;
1181
+ }
1182
+ .color-input-group input {
1183
+ flex-grow: 1;
1184
+ margin-top: 0;
1185
+ }
1186
+ .remove-color-btn {
1187
+ background-color: #ccc;
1188
+ margin-top: 0;
1189
+ padding: 5px 10px;
1190
+ font-size: 0.9rem;
1191
+ border-radius: 5px;
1192
+ }
1193
+ .remove-color-btn:hover {
1194
+ background-color: #bbb;
1195
+ box-shadow: none;
1196
+ transform: none;
1197
+ }
1198
  .add-color-btn {
1199
  background-color: var(--secondary-color);
1200
  }
1201
  .add-color-btn:hover {
1202
  background-color: #BA68C8;
1203
  }
1204
+ .product-images-preview {
1205
+ display: flex;
1206
+ flex-wrap: wrap;
1207
+ gap: 10px;
1208
+ margin-top: 15px;
1209
+ }
1210
+ .product-images-preview img {
1211
+ max-width: 80px;
1212
+ height: auto;
1213
+ border-radius: 8px;
1214
+ border: 1px solid #eee;
1215
+ box-shadow: 0 2px 5px rgba(0,0,0,0.05);
1216
+ }
1217
+ .db-buttons button {
1218
+ margin-right: 10px;
1219
+ }
1220
+ details {
1221
+ margin-top: 15px;
1222
+ border: 1px solid #eee;
1223
+ border-radius: 8px;
1224
+ padding: 10px;
1225
+ background-color: #fafafa;
1226
+ }
1227
+ summary {
1228
+ font-weight: 600;
1229
+ cursor: pointer;
1230
+ color: var(--primary-color);
1231
+ }
1232
  </style>
1233
  </head>
1234
  <body>
 
1237
  <img src="''' + LOGO_URL + '''" alt="Logo" class="header-logo">
1238
  <h1>Админ-панель</h1>
1239
  </div>
1240
+
1241
  <h1>Добавление товара</h1>
1242
  <form method="POST" enctype="multipart/form-data">
1243
  <input type="hidden" name="action" value="add">
1244
+ <label for="add_name">Название товара:</label>
1245
+ <input type="text" id="add_name" name="name" required>
1246
+
1247
+ <label for="add_price">Цена (с):</label>
1248
+ <input type="number" id="add_price" name="price" step="0.01" required>
1249
+
1250
+ <label for="add_description">Описание:</label>
1251
+ <textarea id="add_description" name="description" rows="4" required></textarea>
1252
+
1253
+ <label for="add_category">Категория:</label>
1254
+ <select id="add_category" name="category">
1255
  <option value="Без категории">Без категории</option>
1256
  {% for category in categories %}
1257
+ <option value="{{ category|e }}">{{ category|e }}</option>
1258
  {% endfor %}
1259
  </select>
1260
+
1261
+ <label for="add_photos">Фотографии (до 10, заменят старые при редактировании):</label>
1262
+ <input type="file" id="add_photos" name="photos" accept="image/*" multiple>
1263
+
1264
  <label>Цвета:</label>
1265
+ <div id="add-color-inputs">
1266
  <div class="color-input-group">
1267
  <input type="text" name="colors" placeholder="Например: Красный">
1268
  </div>
1269
  </div>
1270
+ <button type="button" class="add-color-btn" onclick="addColorInput('add-color-inputs')">Добавить цвет</button>
1271
+
1272
  <button type="submit">Добавить товар</button>
1273
  </form>
1274
 
1275
  <h1>Управление категориями</h1>
1276
  <form method="POST">
1277
  <input type="hidden" name="action" value="add_category">
1278
+ <label for="add_category_name">Название новой категории:</label>
1279
+ <input type="text" id="add_category_name" name="category_name" required>
1280
  <button type="submit">Добавить</button>
1281
  </form>
1282
 
 
1284
  <div class="category-list">
1285
  {% for category in categories %}
1286
  <div class="category-item">
1287
+ <h3>{{ category|e }}</h3>
1288
  <form method="POST" style="display: inline;">
1289
  <input type="hidden" name="action" value="delete_category">
1290
+ <input type="hidden" name="category_name_to_delete" value="{{ category|e }}">
1291
+ <button type="submit" class="delete-button" onclick="return confirm('Вы уверены, что хотите удалить категорию «{{ category|e }}»? Товары в этой категории будут перемещены в «Без категории».')">Удалить</button>
1292
  </form>
1293
  </div>
1294
  {% endfor %}
1295
  </div>
1296
 
1297
  <h2>Управление базой данных</h2>
1298
+ <form method="POST" action="{{ url_for('backup') }}" style="display: inline-block;">
1299
+ <button type="submit">Создать резервную копию на HF</button>
1300
+ </form>
1301
+ <form method="GET" action="{{ url_for('download') }}" style="display: inline-block;">
1302
+ <button type="submit">Скачать базу с HF</button>
1303
+ </form>
1304
+
1305
 
1306
+ <h2>Список товаров ({{ products|length }})</h2>
1307
  <div class="product-list">
1308
  {% for product in products %}
1309
  <div class="product-item">
1310
+ <h3>{{ product['name']|e }}</h3>
1311
+ <p><strong>Категория:</strong> {{ product.get('category', 'Без категории')|e }}</p>
1312
  <p><strong>Цена:</strong> {{ product['price'] }} с</p>
1313
+ <p><strong>Описание:</strong> {{ product['description'][:150] }}{% if product['description']|length > 150 %}...{% endif %}</p>
1314
+ <p><strong>Цвета:</strong> {{ product.get('colors', ['Нет цветов'])|join(', ')|e }}</p>
1315
  {% if product.get('photos') and product['photos']|length > 0 %}
1316
+ <div class="product-images-preview">
1317
  {% for photo in product['photos'] %}
1318
+ <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo|e }}"
1319
+ alt="{{ product['name']|e }}"
1320
+ loading="lazy">
1321
  {% endfor %}
1322
  </div>
1323
  {% endif %}
 
1326
  <form method="POST" enctype="multipart/form-data" class="edit-form">
1327
  <input type="hidden" name="action" value="edit">
1328
  <input type="hidden" name="index" value="{{ loop.index0 }}">
1329
+ <label for="edit_name_{{ loop.index0 }}">Название:</label>
1330
+ <input type="text" id="edit_name_{{ loop.index0 }}" name="name" value="{{ product['name']|e }}" required>
1331
+
1332
+ <label for="edit_price_{{ loop.index0 }}">Цена (с):</label>
1333
+ <input type="number" id="edit_price_{{ loop.index0 }}" name="price" step="0.01" value="{{ product['price'] }}" required>
1334
+
1335
+ <label for="edit_description_{{ loop.index0 }}">Описание:</label>
1336
+ <textarea id="edit_description_{{ loop.index0 }}" name="description" rows="4" required>{{ product['description']|e }}</textarea>
1337
+
1338
+ <label for="edit_category_{{ loop.index0 }}">Категория:</label>
1339
+ <select id="edit_category_{{ loop.index0 }}" name="category">
1340
  <option value="Без категории" {% if product.get('category', 'Без категории') == 'Без категории' %}selected{% endif %}>Без категории</option>
1341
  {% for category in categories %}
1342
+ <option value="{{ category|e }}" {% if product.get('category') == category %}selected{% endif %}>{{ category|e }}</option>
1343
  {% endfor %}
1344
  </select>
1345
+
1346
+ <label for="edit_photos_{{ loop.index0 }}">Загрузить новые фотографии (заменят текущие):</label>
1347
+ <input type="file" id="edit_photos_{{ loop.index0 }}" name="photos" accept="image/*" multiple>
1348
+
1349
+ <label>Цвета:</label>
1350
+ <div id="edit-color-inputs-{{ loop.index0 }}">
1351
+ {% for color in product.get('colors', []) %}
1352
+ <div class="color-input-group">
1353
+ <input type="text" name="colors" value="{{ color|e }}">
1354
+ <button type="button" class="remove-color-btn" onclick="removeColorInput(this)">Удалить</button>
1355
+ </div>
1356
+ {% endfor %}
1357
+ </div>
1358
+ <button type="button" class="add-color-btn" onclick="addColorInput('edit-color-inputs-{{ loop.index0 }}')">Добавить цвет</button>
1359
+
1360
+
1361
+ <button type="submit">Сохранить изменения</button>
1362
  </form>
1363
  </details>
1364
+ <form method="POST" style="margin-top: 10px;">
1365
  <input type="hidden" name="action" value="delete">
1366
  <input type="hidden" name="index" value="{{ loop.index0 }}">
1367
+ <button type="submit" class="delete-button" onclick="return confirm('Вы уверены, что хотите удалить товар «{{ product['name']|e }}»?')">Удалить товар</button>
1368
  </form>
1369
  </div>
1370
  {% endfor %}
1371
  </div>
1372
  </div>
1373
  <script>
1374
+ function addColorInput(containerId) {
1375
  const container = document.getElementById(containerId);
1376
+ const newInputGroup = document.createElement('div');
1377
+ newInputGroup.className = 'color-input-group';
1378
+ newInputGroup.innerHTML = `
1379
+ <input type="text" name="colors" placeholder="Например: Красный">
1380
+ <button type="button" class="remove-color-btn" onclick="removeColorInput(this)">Удалить</button>
1381
+ `;
1382
+ container.appendChild(newInputGroup);
1383
+ }
1384
+
1385
+ function removeColorInput(buttonElement) {
1386
+ buttonElement.closest('.color-input-group').remove();
1387
  }
1388
  </script>
1389
  </body>
1390
  </html>
1391
  '''
1392
+ return render_template_string(admin_html, products=products, categories=categories, repo_id=REPO_ID, LOGO_URL=LOGO_URL)
1393
 
1394
  @app.route('/backup', methods=['POST'])
1395
  def backup():
1396
+ try:
1397
+ upload_db_to_hf()
1398
+ return "Резервная копия базы данных успешно создана на Hugging Face.", 200
1399
+ except Exception as e:
1400
+ return f"Ошибка при создании резервной копии: {e}", 500
1401
 
1402
  @app.route('/download', methods=['GET'])
1403
  def download():
1404
+ try:
1405
+ download_db_from_hf()
1406
+ return "База данных успешно скачана с Hugging Face.", 200
1407
+ except Exception as e:
1408
+ return f"Ошибка при скачивании базы данных: {e}", 500
1409
 
1410
  if __name__ == '__main__':
1411
+ # Ensure the uploads directory exists
1412
+ os.makedirs('uploads', exist_ok=True)
1413
+
1414
+ # Start periodic backup thread
1415
  backup_thread = threading.Thread(target=periodic_backup, daemon=True)
1416
  backup_thread.start()
1417
+
1418
+ # Attempt initial data load
1419
  try:
1420
  load_data()
1421
  except Exception as e:
1422
+ logging.error(f"Не удалось выполнить начальную загрузку базы данных при запуске: {e}")
1423
+ # Continue running, the app will likely start with empty data
1424
+
1425
+ # Run the Flask app
1426
  app.run(debug=True, host='0.0.0.0', port=7860)