flpolprojects commited on
Commit
77870ea
·
verified ·
1 Parent(s): 9d3c6b2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +373 -439
app.py CHANGED
@@ -26,7 +26,6 @@ def load_data():
26
  with open(DATA_FILE, 'r', encoding='utf-8') as file:
27
  data = json.load(file)
28
  logging.info("Данные успешно загружены из JSON")
29
- # Проверяем структуру данных и добавляем categories, если его нет
30
  if not isinstance(data, dict) or 'products' not in data or 'categories' not in data:
31
  return {'products': [], 'categories': [] if not isinstance(data, list) else data}
32
  return data
@@ -104,7 +103,8 @@ def catalog():
104
  <meta charset="UTF-8">
105
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
106
  <title>Каталог</title>
107
- <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
 
108
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.css">
109
  <style>
110
  * {
@@ -113,75 +113,107 @@ def catalog():
113
  box-sizing: border-box;
114
  }
115
  body {
116
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
117
- background-color: #f5f5f5;
118
- color: #333;
119
  line-height: 1.6;
120
- padding: 10px;
 
 
 
 
121
  }
122
  .container {
123
- max-width: 1200px;
124
  margin: 0 auto;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  }
126
  .filters-container {
127
- margin-bottom: 20px;
128
  display: flex;
129
  flex-wrap: wrap;
130
  gap: 10px;
131
  justify-content: center;
132
  }
133
  .search-container {
 
134
  text-align: center;
135
- margin-bottom: 20px;
136
  }
137
  #search-input {
138
  width: 90%;
139
- max-width: 500px;
140
- padding: 8px;
141
- font-size: 0.9em;
142
- border: 1px solid #ddd;
143
- border-radius: 5px;
144
  outline: none;
 
 
145
  }
146
  #search-input:focus {
147
- border-color: #3498db;
148
- box-shadow: 0 0 5px rgba(52, 152, 219, 0.5);
149
  }
150
  .category-filter {
151
- padding: 6px 12px;
152
- border: 1px solid #ddd;
153
- border-radius: 5px;
154
- background-color: white;
155
  cursor: pointer;
156
  transition: all 0.3s ease;
 
 
157
  }
158
- .category-filter.active {
159
- background-color: #3498db;
160
  color: white;
161
- border-color: #3498db;
 
162
  }
163
  .products-grid {
164
  display: grid;
165
- grid-template-columns: repeat(2, 1fr);
166
- gap: 10px;
167
- padding: 0 5px;
168
- overflow-y: auto;
169
- max-height: calc(100vh - 160px);
170
  }
171
  .product {
172
- background: #ffffff;
173
- border-radius: 10px;
174
- padding: 10px;
175
- box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
176
- display: flex;
177
- flex-direction: column;
178
- min-width: 0;
 
 
 
179
  }
180
  .product-image {
181
  width: 100%;
182
  aspect-ratio: 1;
183
  background-color: #fff;
184
- border-radius: 8px;
185
  overflow: hidden;
186
  display: flex;
187
  justify-content: center;
@@ -194,76 +226,79 @@ def catalog():
194
  transition: transform 0.3s ease;
195
  }
196
  .product-image img:hover {
197
- transform: scale(1.05);
198
  }
199
  .product h2 {
200
- font-size: 1em;
201
- color: #333;
202
- margin: 10px 0 5px;
203
  font-weight: 600;
 
204
  text-align: center;
 
205
  overflow: hidden;
206
  text-overflow: ellipsis;
207
- white-space: nowrap;
208
  }
209
  .product-price {
210
- font-size: 1.1em;
211
- color: #ff0000;
212
  font-weight: 700;
213
- margin: 5px 0;
214
  text-align: center;
 
215
  }
216
  .product-description {
217
- color: #666;
218
- font-size: 0.8em;
219
- flex-grow: 1;
220
- margin-bottom: 10px;
221
  text-align: center;
 
222
  overflow: hidden;
223
  text-overflow: ellipsis;
224
  white-space: nowrap;
225
  }
226
  .product-button {
227
- background-color: #3498db;
228
- color: white;
229
- padding: 6px 12px;
230
  border: none;
231
- border-radius: 5px;
 
 
 
 
232
  cursor: pointer;
233
- transition: background-color 0.3s ease;
 
234
  text-align: center;
235
  text-decoration: none;
236
- display: block;
237
- margin: 5px auto;
238
- font-size: 0.8em;
239
  }
240
  .product-button:hover {
241
- background-color: #2980b9;
 
242
  }
243
  .add-to-cart {
244
- background-color: #27ae60;
245
  }
246
  .add-to-cart:hover {
247
- background-color: #219653;
248
  }
249
  #cart-button {
250
  position: fixed;
251
- bottom: 15px;
252
- right: 15px;
253
- background-color: #e74c3c;
254
  color: white;
255
  border: none;
256
  border-radius: 50%;
257
- width: 40px;
258
- height: 40px;
259
- font-size: 16px;
260
  cursor: pointer;
261
  display: none;
262
- box-shadow: 0 2px 10px rgba(0,0,0,0.2);
 
263
  z-index: 1000;
264
  }
265
  #cart-button:hover {
266
- background-color: #c0392b;
 
267
  }
268
  .modal {
269
  display: none;
@@ -273,105 +308,100 @@ def catalog():
273
  top: 0;
274
  width: 100%;
275
  height: 100%;
276
- overflow: auto;
277
- background-color: rgba(0,0,0,0.4);
278
  }
279
  .modal-content {
280
- position: relative;
281
- background-color: #fefefe;
282
- margin: 10% auto;
283
- padding: 15px;
284
- border: 1px solid #888;
285
  width: 90%;
286
- max-width: 600px;
287
- box-shadow: 0 4px 8px rgba(0,0,0,0.2);
288
- animation: animatetop 0.4s;
289
  }
290
- @keyframes animatetop {
291
- from {top: -300px; opacity: 0}
292
- to {top: 10%; opacity: 1}
293
  }
294
  .close {
295
- color: #aaa;
296
  float: right;
297
- font-size: 24px;
298
- font-weight: bold;
299
  cursor: pointer;
 
300
  }
301
  .close:hover {
302
- color: black;
303
  }
304
  .cart-item {
305
  display: flex;
306
  justify-content: space-between;
307
  align-items: center;
308
- padding: 10px 0;
309
- border-bottom: 1px solid #eee;
310
  }
311
  .cart-item img {
312
- width: 40px;
313
- height: 40px;
314
  object-fit: contain;
315
- background-color: #fff;
316
- margin-right: 10px;
317
  }
318
  .quantity-input {
319
- width: 50px;
320
- padding: 5px;
321
- margin: 10px 0;
322
- font-size: 0.9em;
 
323
  }
324
  .clear-cart {
325
- background-color: #e74c3c;
326
- margin-top: 10px;
327
- margin-right: 10px;
328
  }
329
  .clear-cart:hover {
330
- background-color: #c0392b;
331
  }
332
  .order-button {
333
  background-color: #25D366;
334
- margin-top: 10px;
335
  }
336
  .order-button:hover {
337
  background-color: #20B956;
338
  }
339
- @media (min-width: 768px) {
340
  .products-grid {
341
- grid-template-columns: repeat(2, 1fr);
342
- gap: 20px;
343
- padding: 0 15px;
344
- max-height: none;
345
- }
346
- .product {
347
- padding: 15px;
348
- }
349
- .product-image {
350
- aspect-ratio: 1;
351
  }
352
  .product h2 {
353
- font-size: 1.2em;
354
  }
355
  .product-price {
356
- font-size: 1.3em;
357
  }
358
  .product-description {
359
- font-size: 0.9em;
360
  }
361
  .product-button {
362
- font-size: 0.9em;
363
- padding: 8px 15px;
364
  }
365
  #cart-button {
366
  width: 50px;
367
  height: 50px;
368
- font-size: 18px;
369
  }
370
  }
371
  </style>
372
  </head>
373
  <body>
374
  <div class="container">
 
 
 
 
 
 
375
  <div class="filters-container">
376
  <button class="category-filter active" data-category="all">Все категории</button>
377
  {% for category in categories %}
@@ -379,7 +409,7 @@ def catalog():
379
  {% endfor %}
380
  </div>
381
  <div class="search-container">
382
- <input type="text" id="search-input" placeholder="Поиск по названию или описанию...">
383
  </div>
384
  <div class="products-grid" id="products-grid">
385
  {% for product in products %}
@@ -395,7 +425,7 @@ def catalog():
395
  </div>
396
  {% endif %}
397
  <h2>{{ product['name'] }}</h2>
398
- <div class="product-price">{{ product['price'] }}</div>
399
  <p class="product-description">{{ product['description'][:50] }}{% if product['description']|length > 50 %}...{% endif %}</p>
400
  <button class="product-button" onclick="openModal({{ loop.index0 }})">Подробнее</button>
401
  <button class="product-button add-to-cart" onclick="openQuantityModal({{ loop.index0 }})">В корзину</button>
@@ -428,9 +458,9 @@ def catalog():
428
  <span class="close" onclick="closeModal('cartModal')">×</span>
429
  <h2>Корзина</h2>
430
  <div id="cartContent"></div>
431
- <div style="margin-top: 15px; text-align: right;">
432
- <strong>Итого: <span id="cartTotal">0</span></strong>
433
- <button class="product-button clear-cart" onclick="clearCart()">Очистить корзину</button>
434
  <button class="product-button order-button" onclick="orderViaWhatsApp()">Заказать</button>
435
  </div>
436
  </div>
@@ -440,20 +470,30 @@ def catalog():
440
 
441
  <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
442
  <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.5.3/dist/umd/popper.min.js"></script>
443
- <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
444
  <script src="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.js"></script>
445
  <script>
446
  const products = {{ products|tojson }};
447
  let selectedProductIndex = null;
448
 
 
 
 
 
 
 
 
 
 
 
 
 
 
449
  function openModal(index) {
450
- console.log("Открытие модального окна для товара:", index);
451
  loadProductDetails(index);
452
  document.getElementById('productModal').style.display = "block";
453
  }
454
 
455
  function closeModal(modalId) {
456
- console.log("Закрытие модального окна:", modalId);
457
  document.getElementById(modalId).style.display = "none";
458
  }
459
 
@@ -464,7 +504,7 @@ def catalog():
464
  document.getElementById('modalContent').innerHTML = data;
465
  initializeSwiper();
466
  })
467
- .catch(error => console.error('Ошибка загрузки деталей:', error));
468
  }
469
 
470
  function initializeSwiper() {
@@ -473,111 +513,71 @@ def catalog():
473
  spaceBetween: 20,
474
  loop: true,
475
  grabCursor: true,
476
- pagination: {
477
- el: '.swiper-pagination',
478
- clickable: true,
479
- },
480
- navigation: {
481
- nextEl: '.swiper-button-next',
482
- prevEl: '.swiper-button-prev',
483
- },
484
- zoom: {
485
- maxRatio: 3,
486
- },
487
  });
488
  }
489
 
490
  function openQuantityModal(index) {
491
- console.log("Открытие окна количества для товара:", index);
492
  selectedProductIndex = index;
493
  document.getElementById('quantityModal').style.display = 'block';
494
  document.getElementById('quantityInput').value = 1;
495
  }
496
 
497
  function confirmAddToCart() {
498
- console.log("Подтверждение добавления в корзину, индекс:", selectedProductIndex);
499
- if (selectedProductIndex === null || selectedProductIndex === undefined) {
500
- console.error("Товар не выбран!");
501
- alert("Ошибка: Товар не выбран");
502
- return;
503
- }
504
-
505
- const quantityInput = document.getElementById('quantityInput');
506
- const quantity = parseInt(quantityInput.value) || 1;
507
-
508
  if (quantity <= 0) {
509
- console.warn("Некорректное количество:", quantity);
510
- alert("Пожалуйста, укажите количество больше 0");
511
  return;
512
  }
513
-
514
  let cart = JSON.parse(localStorage.getItem('cart') || '[]');
515
  const product = products[selectedProductIndex];
516
-
517
- if (!product) {
518
- console.error("Продукт не найден по индексу:", selectedProductIndex);
519
- alert("Ошибка: Товар не найден");
520
- return;
521
- }
522
-
523
  const existingItem = cart.find(item => item.name === product.name);
524
 
525
  if (existingItem) {
526
  existingItem.quantity += quantity;
527
- console.log("Увеличено количество существующего товара:", product.name, "на", quantity);
528
  } else {
529
- const newItem = {
530
  name: product.name,
531
  price: product.price,
532
  photo: product.photos && product.photos.length > 0 ? product.photos[0] : '',
533
  quantity: quantity
534
- };
535
- cart.push(newItem);
536
- console.log("Добавлен новый товар в корзину:", newItem);
537
  }
538
 
539
  localStorage.setItem('cart', JSON.stringify(cart));
540
- console.log("Корзина обновлена:", cart);
541
  closeModal('quantityModal');
542
  updateCartButton();
543
  }
544
 
545
  function updateCartButton() {
546
  const cart = JSON.parse(localStorage.getItem('cart') || '[]');
547
- const cartButton = document.getElementById('cart-button');
548
- cartButton.style.display = cart.length > 0 ? 'block' : 'none';
549
- console.log("Обновление кнопки корзины, элементов в корзине:", cart.length);
550
  }
551
 
552
  function openCartModal() {
553
- console.log("Открытие корзины");
554
  const cart = JSON.parse(localStorage.getItem('cart') || '[]');
555
  const cartContent = document.getElementById('cartContent');
556
  let total = 0;
557
 
558
- if (cart.length === 0) {
559
- cartContent.innerHTML = '<p>Корзина пуста</p>';
560
- } else {
561
- cartContent.innerHTML = cart.map(item => {
562
- const itemTotal = item.price * item.quantity;
563
- total += itemTotal;
564
- return `
565
- <div class="cart-item">
566
- <div style="display: flex; align-items: center;">
567
- ${item.photo ? `
568
- <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/${item.photo}"
569
- alt="${item.name}">
570
- ` : ''}
571
- <div>
572
- <strong>${item.name}</strong>
573
- <p>${item.price} × ${item.quantity}</p>
574
- </div>
575
  </div>
576
- <span>${itemTotal}</span>
577
  </div>
578
- `;
579
- }).join('');
580
- }
 
581
 
582
  document.getElementById('cartTotal').textContent = total;
583
  document.getElementById('cartModal').style.display = 'block';
@@ -586,47 +586,34 @@ def catalog():
586
  function orderViaWhatsApp() {
587
  const cart = JSON.parse(localStorage.getItem('cart') || '[]');
588
  if (cart.length === 0) {
589
- alert("Корзина пуста! Добавьте товары перед заказом.");
590
  return;
591
  }
592
-
593
  let total = 0;
594
  let orderText = "Заказ:%0A";
595
  cart.forEach((item, index) => {
596
  const itemTotal = item.price * item.quantity;
597
  total += itemTotal;
598
- orderText += `${index + 1}. ${item.name} - ${item.price} × ${item.quantity}%0A`;
599
  });
600
- orderText += `Итого: ${total}`;
601
-
602
- const whatsappUrl = `https://api.whatsapp.com/send?phone=996500398754&text=${orderText}`;
603
- window.open(whatsappUrl, '_blank');
604
- console.log("Переход в WhatsApp с заказом:", orderText);
605
  }
606
 
607
  function clearCart() {
608
- console.log("Очистка корзины");
609
  localStorage.removeItem('cart');
610
  closeModal('cartModal');
611
  updateCartButton();
612
  }
613
 
614
  window.onclick = function(event) {
615
- if (event.target.className === 'modal') {
616
- console.log("Закрытие модального окна по клику вне области");
617
- event.target.style.display = "none";
618
- }
619
  }
620
 
621
- // Поисковая строка и фильтрация
622
- document.getElementById('search-input').addEventListener('input', function(e) {
623
- filterProducts();
624
- });
625
-
626
- const categoryFilters = document.querySelectorAll('.category-filter');
627
- categoryFilters.forEach(filter => {
628
  filter.addEventListener('click', function() {
629
- categoryFilters.forEach(f => f.classList.remove('active'));
630
  this.classList.add('active');
631
  filterProducts();
632
  });
@@ -635,25 +622,16 @@ def catalog():
635
  function filterProducts() {
636
  const searchTerm = document.getElementById('search-input').value.toLowerCase();
637
  const activeCategory = document.querySelector('.category-filter.active').dataset.category;
638
- const productElements = document.querySelectorAll('.product');
639
-
640
- productElements.forEach(product => {
641
  const name = product.getAttribute('data-name');
642
  const description = product.getAttribute('data-description');
643
  const category = product.getAttribute('data-category');
644
-
645
  const matchesSearch = name.includes(searchTerm) || description.includes(searchTerm);
646
  const matchesCategory = activeCategory === 'all' || category === activeCategory;
647
-
648
- if (matchesSearch && matchesCategory) {
649
- product.style.display = 'flex';
650
- } else {
651
- product.style.display = 'none';
652
- }
653
  });
654
  }
655
 
656
- console.log("Инициализация страницы, товары:", products);
657
  updateCartButton();
658
  </script>
659
  </body>
@@ -670,9 +648,9 @@ def product_detail(index):
670
  except IndexError:
671
  return "Продукт не найден", 404
672
  detail_html = '''
673
- <div class="container">
674
- <h2>{{ product['name'] }}</h2>
675
- <div class="swiper-container">
676
  <div class="swiper-wrapper">
677
  {% if product.get('photos') %}
678
  {% for photo in product['photos'] %}
@@ -680,13 +658,13 @@ def product_detail(index):
680
  <div class="swiper-zoom-container">
681
  <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo }}"
682
  alt="{{ product['name'] }}"
683
- style="max-width: 200px; max-height: 200px; width: auto; height: auto; object-fit: contain;">
684
  </div>
685
  </div>
686
  {% endfor %}
687
  {% else %}
688
  <div class="swiper-slide">
689
- <img src="https://via.placeholder.com/200" alt="No Image">
690
  </div>
691
  {% endif %}
692
  </div>
@@ -695,7 +673,7 @@ def product_detail(index):
695
  <div class="swiper-button-prev"></div>
696
  </div>
697
  <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
698
- <p><strong>Цена:</strong> {{ product['price'] }}</p>
699
  <p><strong>Описание:</strong> {{ product['description'] }}</p>
700
  </div>
701
  '''
@@ -710,29 +688,23 @@ def admin():
710
  if request.method == 'POST':
711
  action = request.form.get('action')
712
 
713
- # Обработка категорий
714
  if action == 'add_category':
715
  category_name = request.form.get('category_name')
716
  if category_name and category_name not in categories:
717
  categories.append(category_name)
718
  save_data(data)
719
- logging.info(f"Добавлена новая категория: {category_name}")
720
  return redirect(url_for('admin'))
721
- else:
722
- return "Ошибка: Категория уже существует или не указано название", 400
723
 
724
  elif action == 'delete_category':
725
  category_index = int(request.form.get('category_index'))
726
  deleted_category = categories.pop(category_index)
727
- # Обновляем продукты, убирая удаленную категорию
728
  for product in products:
729
  if product.get('category') == deleted_category:
730
  product['category'] = 'Без категории'
731
  save_data(data)
732
- logging.info(f"Удалена категория: {deleted_category}")
733
  return redirect(url_for('admin'))
734
 
735
- # Обработка продуктов
736
  elif action == 'add':
737
  name = request.form.get('name')
738
  price = request.form.get('price')
@@ -741,45 +713,31 @@ def admin():
741
  photos_files = request.files.getlist('photos')
742
  photos_list = []
743
 
744
- logging.info(f"Добавление нового товара: name={name}, price={price}, description={description}, category={category}")
745
-
746
  if photos_files:
747
- for i, photo in enumerate(photos_files[:2]):
748
  if photo and photo.filename:
749
  photo_filename = secure_filename(photo.filename)
750
  uploads_dir = 'uploads'
751
  os.makedirs(uploads_dir, exist_ok=True)
752
  temp_path = os.path.join(uploads_dir, photo_filename)
753
  photo.save(temp_path)
754
- try:
755
- api = HfApi()
756
- api.upload_file(
757
- path_or_fileobj=temp_path,
758
- path_in_repo=f"photos/{photo_filename}",
759
- repo_id=REPO_ID,
760
- repo_type="dataset",
761
- token=HF_TOKEN_WRITE,
762
- commit_message=f"Добавлено фото для товара {name}"
763
- )
764
- photos_list.append(photo_filename)
765
- logging.info(f"Фото успешно загружено: {photo_filename}")
766
- except Exception as e:
767
- logging.error(f"Ошибка при загрузке фото: {e}")
768
- return f"Ошибка при загрузке фото: {e}", 500
769
- finally:
770
- if os.path.exists(temp_path):
771
- os.remove(temp_path)
772
 
773
  if not name or not price or not description:
774
- logging.error("Ошибка: Не все обязательные поля заполнены")
775
  return "Ошибка: Заполните все обязательные поля", 400
776
 
777
- try:
778
- price = float(price.replace(',', '.'))
779
- except ValueError:
780
- logging.error("Ошибка: Цена должна быть числом")
781
- return "Ошибка: Цена должна быть числом", 400
782
-
783
  new_product = {
784
  'name': name,
785
  'price': price,
@@ -787,10 +745,8 @@ def admin():
787
  'category': category if category in categories else 'Без категории',
788
  'photos': photos_list
789
  }
790
-
791
  products.append(new_product)
792
  save_data(data)
793
- logging.info(f"Новый товар добавлен: {new_product}")
794
  return redirect(url_for('admin'))
795
 
796
  elif action == 'edit':
@@ -801,56 +757,40 @@ def admin():
801
  category = request.form.get('category')
802
  photos_files = request.files.getlist('photos')
803
 
804
- logging.info(f"Редактирование товара с индексом {index}")
805
-
806
  if photos_files and any(photo.filename for photo in photos_files):
807
  new_photos_list = []
808
- for i, photo in enumerate(photos_files[:2]):
809
  if photo and photo.filename:
810
  photo_filename = secure_filename(photo.filename)
811
  uploads_dir = 'uploads'
812
  os.makedirs(uploads_dir, exist_ok=True)
813
  temp_path = os.path.join(uploads_dir, photo_filename)
814
  photo.save(temp_path)
815
- try:
816
- api = HfApi()
817
- api.upload_file(
818
- path_or_fileobj=temp_path,
819
- path_in_repo=f"photos/{photo_filename}",
820
- repo_id=REPO_ID,
821
- repo_type="dataset",
822
- token=HF_TOKEN_WRITE,
823
- commit_message=f"Обновлено фото для товара {name}"
824
- )
825
- new_photos_list.append(photo_filename)
826
- logging.info(f"Фото обновлено: {photo_filename}")
827
- except Exception as e:
828
- logging.error(f"Ошибка при загрузке фото: {e}")
829
- return f"Ошибка при загрузке фото: {e}", 500
830
- finally:
831
- if os.path.exists(temp_path):
832
- os.remove(temp_path)
833
  products[index]['photos'] = new_photos_list
834
 
835
  products[index]['name'] = name
836
- try:
837
- products[index]['price'] = float(price.replace(',', '.'))
838
- except ValueError:
839
- logging.error("Ошибка: Цена должна быть числом")
840
- return "Ошибка: Цена должна быть числом", 400
841
  products[index]['description'] = description
842
  products[index]['category'] = category if category in categories else 'Без категории'
843
-
844
  save_data(data)
845
- logging.info(f"Товар с индексом {index} успешно отредактирован")
846
  return redirect(url_for('admin'))
847
 
848
  elif action == 'delete':
849
  index = int(request.form.get('index'))
850
- logging.info(f"Удаление товара с индексом {index}")
851
  del products[index]
852
  save_data(data)
853
- logging.info("Товар успешно удален")
854
  return redirect(url_for('admin'))
855
 
856
  admin_html = '''
@@ -860,188 +800,184 @@ def admin():
860
  <meta charset="UTF-8">
861
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
862
  <title>Админ-панель</title>
 
863
  <style>
864
  body {
865
- font-family: Arial, sans-serif;
866
- margin: 20px;
867
- background-color: #f9f9f9;
 
 
 
 
 
868
  }
869
  h1, h2 {
870
- color: #333;
 
871
  }
872
  form {
873
- background-color: #fff;
874
- padding: 15px;
875
- border: 1px solid #ddd;
876
- border-radius: 5px;
877
- max-width: 100%;
878
- margin-bottom: 20px;
879
  }
880
  label {
 
 
881
  display: block;
882
- margin-top: 10px;
883
- color: #555;
884
  }
885
  input, textarea, select {
886
  width: 100%;
887
- padding: 8px;
888
  margin-top: 5px;
889
- border: 1px solid #ddd;
890
- border-radius: 4px;
891
- box-sizing: border-box;
 
 
 
 
 
 
892
  }
893
  button {
894
- margin-top: 10px;
895
- padding: 8px 12px;
896
- background-color: #28a745;
897
- color: white;
898
  border: none;
899
- border-radius: 4px;
 
 
 
900
  cursor: pointer;
 
 
901
  }
902
  button:hover {
903
- background-color: #218838;
 
904
  }
905
  .delete-button {
906
- background-color: #dc3545;
907
  }
908
  .delete-button:hover {
909
- background-color: #c82333;
910
  }
911
  .product-list, .category-list {
912
- margin-top: 20px;
 
913
  }
914
  .product-item, .category-item {
915
- background-color: #fff;
916
- border: 1px solid #ddd;
917
- padding: 10px;
918
- margin-bottom: 10px;
919
- border-radius: 5px;
920
  }
921
  .edit-form {
922
- margin-top: 10px;
923
- padding: 10px;
924
- border: 1px solid #ddd;
925
- border-radius: 5px;
926
- background-color: #f9f9f9;
927
- }
928
- @media (max-width: 768px) {
929
- body {
930
- margin: 10px;
931
- }
932
- form {
933
- padding: 10px;
934
- }
935
- input, textarea, select {
936
- font-size: 0.9em;
937
- }
938
- button {
939
- padding: 6px 10px;
940
- font-size: 0.9em;
941
- }
942
- .product-item, .category-item {
943
- padding: 8px;
944
- }
945
  }
946
  </style>
947
  </head>
948
  <body>
949
- <h1>Добавление товара</h1>
950
- <form method="POST" enctype="multipart/form-data">
951
- <input type="hidden" name="action" value="add">
952
- <label for="name">Название товара:</label>
953
- <input type="text" id="name" name="name" required>
954
- <label for="price">Цена:</label>
955
- <input type="number" id="price" name="price" step="0.01" required>
956
- <label for="description">Описание:</label>
957
- <textarea id="description" name="description" rows="4" required></textarea>
958
- <label for="category">Категория:</label>
959
- <select id="category" name="category">
960
- <option value="Без категории">Без категории</option>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
961
  {% for category in categories %}
962
- <option value="{{ category }}">{{ category }}</option>
 
 
 
 
 
 
 
963
  {% endfor %}
964
- </select>
965
- <label for="photos">Фотографии товара (максимум 2):</label>
966
- <input type="file" id="photos" name="photos" accept="image/*" multiple>
967
- <button type="submit">Добавить товар</button>
968
- </form>
969
-
970
- <h1>Управление категориями</h1>
971
- <form method="POST">
972
- <input type="hidden" name="action" value="add_category">
973
- <label for="category_name">Название категории:</label>
974
- <input type="text" id="category_name" name="category_name" required>
975
- <button type="submit">Добавить категорию</button>
976
- </form>
977
-
978
- <h2>Список категорий</h2>
979
- <div class="category-list">
980
- {% for category in categories %}
981
- <div class="category-item">
982
- <h3>{{ category }}</h3>
983
- <form method="POST" style="display: inline;">
984
- <input type="hidden" name="action" value="delete_category">
985
- <input type="hidden" name="category_index" value="{{ loop.index0 }}">
986
- <button type="submit" class="delete-button">Удалить</button>
987
- </form>
988
  </div>
989
- {% endfor %}
990
- </div>
991
 
992
- <h2>Управление базой данных</h2>
993
- <form method="POST" action="{{ url_for('backup') }}">
994
- <button type="submit">Создать резервную копию</button>
995
- </form>
996
- <form method="GET" action="{{ url_for('download') }}">
997
- <button type="submit">Скачать базу данных</button>
998
- </form>
999
 
1000
- <h2>Список товаров</h2>
1001
- <div class="product-list">
1002
- {% for product in products %}
1003
- <div class="product-item">
1004
- <h3>{{ product['name'] }}</h3>
1005
- <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
1006
- <p><strong>Цена:</strong> {{ product['price'] }}</p>
1007
- <p><strong>Описание:</strong> {{ product['description'] }}</p>
1008
- {% if product.get('photos') and product['photos']|length > 0 %}
1009
- <div style="background-color: #fff; width: 100px; height: 100px; display: flex; justify-content: center; align-items: center;">
1010
- <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product['photos'][0] }}"
1011
- alt="{{ product['name'] }}"
1012
- style="max-width: 100px; max-height: 100px; width: auto; height: auto; object-fit: contain;">
1013
- </div>
1014
- {% endif %}
1015
- <details>
1016
- <summary>Редактировать</summary>
1017
- <form method="POST" enctype="multipart/form-data" class="edit-form">
1018
- <input type="hidden" name="action" value="edit">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1019
  <input type="hidden" name="index" value="{{ loop.index0 }}">
1020
- <label for="name">Название товара:</label>
1021
- <input type="text" id="name" name="name" value="{{ product['name'] }}" required>
1022
- <label for="price">Цена:</label>
1023
- <input type="number" id="price" name="price" step="0.01" value="{{ product['price'] }}" required>
1024
- <label for="description">Описание:</label>
1025
- <textarea id="description" name="description" rows="4" required>{{ product['description'] }}</textarea>
1026
- <label for="category">Категория:</label>
1027
- <select id="category" name="category">
1028
- <option value="Без категории" {% if product.get('category', 'Без категории') == 'Без категории' %}selected{% endif %}>Без категории</option>
1029
- {% for category in categories %}
1030
- <option value="{{ category }}" {% if product.get('category') == category %}selected{% endif %}>{{ category }}</option>
1031
- {% endfor %}
1032
- </select>
1033
- <label for="photos">Фотографии товара (максимум 2):</label>
1034
- <input type="file" id="photos" name="photos" accept="image/*" multiple>
1035
- <button type="submit">Сохранить изменения</button>
1036
  </form>
1037
- </details>
1038
- <form method="POST">
1039
- <input type="hidden" name="action" value="delete">
1040
- <input type="hidden" name="index" value="{{ loop.index0 }}">
1041
- <button type="submit" class="delete-button">Удалить</button>
1042
- </form>
1043
  </div>
1044
- {% endfor %}
1045
  </div>
1046
  </body>
1047
  </html>
@@ -1051,20 +987,18 @@ def admin():
1051
  @app.route('/backup', methods=['POST'])
1052
  def backup():
1053
  upload_db_to_hf()
1054
- return "Резервная копия успешно создана.", 200
1055
 
1056
  @app.route('/download', methods=['GET'])
1057
  def download():
1058
  download_db_from_hf()
1059
- return "База данных успешно скачана.", 200
1060
 
1061
  if __name__ == '__main__':
1062
  backup_thread = threading.Thread(target=periodic_backup, daemon=True)
1063
  backup_thread.start()
1064
-
1065
  try:
1066
  load_data()
1067
  except Exception as e:
1068
- logging.error(f"Не удалось загрузить базу данных при запуске: {e}")
1069
-
1070
  app.run(debug=True, host='0.0.0.0', port=7860)
 
26
  with open(DATA_FILE, 'r', encoding='utf-8') as file:
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
 
103
  <meta charset="UTF-8">
104
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
105
  <title>Каталог</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
  * {
 
113
  box-sizing: border-box;
114
  }
115
  body {
116
+ font-family: 'Poppins', sans-serif;
117
+ background: linear-gradient(135deg, #f0f2f5, #e9ecef);
118
+ color: #2d3748;
119
  line-height: 1.6;
120
+ transition: background 0.3s, color 0.3s;
121
+ }
122
+ body.dark-mode {
123
+ background: linear-gradient(135deg, #1a202c, #2d3748);
124
+ color: #e2e8f0;
125
  }
126
  .container {
127
+ max-width: 1300px;
128
  margin: 0 auto;
129
+ padding: 20px;
130
+ }
131
+ .header {
132
+ display: flex;
133
+ justify-content: space-between;
134
+ align-items: center;
135
+ padding: 15px 0;
136
+ border-bottom: 1px solid #e2e8f0;
137
+ }
138
+ .header h1 {
139
+ font-size: 1.5rem;
140
+ font-weight: 600;
141
+ }
142
+ .theme-toggle {
143
+ background: none;
144
+ border: none;
145
+ font-size: 1.5rem;
146
+ cursor: pointer;
147
+ color: #4a5568;
148
+ }
149
+ .theme-toggle:hover {
150
+ color: #2b6cb0;
151
  }
152
  .filters-container {
153
+ margin: 20px 0;
154
  display: flex;
155
  flex-wrap: wrap;
156
  gap: 10px;
157
  justify-content: center;
158
  }
159
  .search-container {
160
+ margin: 20px 0;
161
  text-align: center;
 
162
  }
163
  #search-input {
164
  width: 90%;
165
+ max-width: 600px;
166
+ padding: 12px 18px;
167
+ font-size: 1rem;
168
+ border: 1px solid #e2e8f0;
169
+ border-radius: 25px;
170
  outline: none;
171
+ box-shadow: 0 2px 5px rgba(0,0,0,0.05);
172
+ transition: all 0.3s ease;
173
  }
174
  #search-input:focus {
175
+ border-color: #2b6cb0;
176
+ box-shadow: 0 4px 15px rgba(43, 108, 176, 0.2);
177
  }
178
  .category-filter {
179
+ padding: 8px 16px;
180
+ border: 1px solid #e2e8f0;
181
+ border-radius: 20px;
182
+ background-color: #fff;
183
  cursor: pointer;
184
  transition: all 0.3s ease;
185
+ font-size: 0.9rem;
186
+ font-weight: 400;
187
  }
188
+ .category-filter.active, .category-filter:hover {
189
+ background-color: #2b6cb0;
190
  color: white;
191
+ border-color: #2b6cb0;
192
+ box-shadow: 0 2px 10px rgba(43, 108, 176, 0.3);
193
  }
194
  .products-grid {
195
  display: grid;
196
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
197
+ gap: 20px;
198
+ padding: 10px;
 
 
199
  }
200
  .product {
201
+ background: #fff;
202
+ border-radius: 15px;
203
+ padding: 15px;
204
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
205
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
206
+ overflow: hidden;
207
+ }
208
+ .product:hover {
209
+ transform: translateY(-5px);
210
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
211
  }
212
  .product-image {
213
  width: 100%;
214
  aspect-ratio: 1;
215
  background-color: #fff;
216
+ border-radius: 10px;
217
  overflow: hidden;
218
  display: flex;
219
  justify-content: center;
 
226
  transition: transform 0.3s ease;
227
  }
228
  .product-image img:hover {
229
+ transform: scale(1.1);
230
  }
231
  .product h2 {
232
+ font-size: 1.2rem;
 
 
233
  font-weight: 600;
234
+ margin: 10px 0;
235
  text-align: center;
236
+ white-space: nowrap;
237
  overflow: hidden;
238
  text-overflow: ellipsis;
 
239
  }
240
  .product-price {
241
+ font-size: 1.3rem;
242
+ color: #e53e3e;
243
  font-weight: 700;
 
244
  text-align: center;
245
+ margin: 5px 0;
246
  }
247
  .product-description {
248
+ font-size: 0.9rem;
249
+ color: #718096;
 
 
250
  text-align: center;
251
+ margin-bottom: 15px;
252
  overflow: hidden;
253
  text-overflow: ellipsis;
254
  white-space: nowrap;
255
  }
256
  .product-button {
257
+ display: block;
258
+ width: 100%;
259
+ padding: 10px;
260
  border: none;
261
+ border-radius: 25px;
262
+ background-color: #2b6cb0;
263
+ color: white;
264
+ font-size: 0.9rem;
265
+ font-weight: 500;
266
  cursor: pointer;
267
+ transition: all 0.3s ease;
268
+ margin: 5px 0;
269
  text-align: center;
270
  text-decoration: none;
 
 
 
271
  }
272
  .product-button:hover {
273
+ background-color: #2c5282;
274
+ box-shadow: 0 2px 10px rgba(44, 82, 130, 0.4);
275
  }
276
  .add-to-cart {
277
+ background-color: #38a169;
278
  }
279
  .add-to-cart:hover {
280
+ background-color: #2f855a;
281
  }
282
  #cart-button {
283
  position: fixed;
284
+ bottom: 20px;
285
+ right: 20px;
286
+ background-color: #e53e3e;
287
  color: white;
288
  border: none;
289
  border-radius: 50%;
290
+ width: 60px;
291
+ height: 60px;
292
+ font-size: 1.5rem;
293
  cursor: pointer;
294
  display: none;
295
+ box-shadow: 0 4px 15px rgba(229, 62, 62, 0.4);
296
+ transition: all 0.3s ease;
297
  z-index: 1000;
298
  }
299
  #cart-button:hover {
300
+ background-color: #c53030;
301
+ transform: scale(1.1);
302
  }
303
  .modal {
304
  display: none;
 
308
  top: 0;
309
  width: 100%;
310
  height: 100%;
311
+ background-color: rgba(0,0,0,0.5);
312
+ backdrop-filter: blur(5px);
313
  }
314
  .modal-content {
315
+ background: #fff;
316
+ margin: 5% auto;
317
+ padding: 20px;
318
+ border-radius: 15px;
 
319
  width: 90%;
320
+ max-width: 700px;
321
+ box-shadow: 0 10px 30px rgba(0,0,0,0.2);
322
+ animation: slideIn 0.3s ease-out;
323
  }
324
+ @keyframes slideIn {
325
+ from { transform: translateY(-50px); opacity: 0; }
326
+ to { transform: translateY(0); opacity: 1; }
327
  }
328
  .close {
 
329
  float: right;
330
+ font-size: 1.5rem;
331
+ color: #718096;
332
  cursor: pointer;
333
+ transition: color 0.3s;
334
  }
335
  .close:hover {
336
+ color: #2d3748;
337
  }
338
  .cart-item {
339
  display: flex;
340
  justify-content: space-between;
341
  align-items: center;
342
+ padding: 15px 0;
343
+ border-bottom: 1px solid #e2e8f0;
344
  }
345
  .cart-item img {
346
+ width: 50px;
347
+ height: 50px;
348
  object-fit: contain;
349
+ border-radius: 8px;
350
+ margin-right: 15px;
351
  }
352
  .quantity-input {
353
+ width: 70px;
354
+ padding: 8px;
355
+ border: 1px solid #e2e8f0;
356
+ border-radius: 10px;
357
+ font-size: 1rem;
358
  }
359
  .clear-cart {
360
+ background-color: #e53e3e;
 
 
361
  }
362
  .clear-cart:hover {
363
+ background-color: #c53030;
364
  }
365
  .order-button {
366
  background-color: #25D366;
 
367
  }
368
  .order-button:hover {
369
  background-color: #20B956;
370
  }
371
+ @media (max-width: 768px) {
372
  .products-grid {
373
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
374
+ gap: 15px;
 
 
 
 
 
 
 
 
375
  }
376
  .product h2 {
377
+ font-size: 1rem;
378
  }
379
  .product-price {
380
+ font-size: 1.1rem;
381
  }
382
  .product-description {
383
+ font-size: 0.8rem;
384
  }
385
  .product-button {
386
+ font-size: 0.8rem;
387
+ padding: 8px;
388
  }
389
  #cart-button {
390
  width: 50px;
391
  height: 50px;
392
+ font-size: 1.2rem;
393
  }
394
  }
395
  </style>
396
  </head>
397
  <body>
398
  <div class="container">
399
+ <div class="header">
400
+ <h1>Каталог</h1>
401
+ <button class="theme-toggle" onclick="toggleTheme()">
402
+ <i class="fas fa-moon"></i>
403
+ </button>
404
+ </div>
405
  <div class="filters-container">
406
  <button class="category-filter active" data-category="all">Все категории</button>
407
  {% for category in categories %}
 
409
  {% endfor %}
410
  </div>
411
  <div class="search-container">
412
+ <input type="text" id="search-input" placeholder="Поиск товаров...">
413
  </div>
414
  <div class="products-grid" id="products-grid">
415
  {% for product in products %}
 
425
  </div>
426
  {% endif %}
427
  <h2>{{ product['name'] }}</h2>
428
+ <div class="product-price">{{ product['price'] }} ₽</div>
429
  <p class="product-description">{{ product['description'][:50] }}{% if product['description']|length > 50 %}...{% endif %}</p>
430
  <button class="product-button" onclick="openModal({{ loop.index0 }})">Подробнее</button>
431
  <button class="product-button add-to-cart" onclick="openQuantityModal({{ loop.index0 }})">В корзину</button>
 
458
  <span class="close" onclick="closeModal('cartModal')">×</span>
459
  <h2>Корзина</h2>
460
  <div id="cartContent"></div>
461
+ <div style="margin-top: 20px; text-align: right;">
462
+ <strong>Итого: <span id="cartTotal">0</span> ₽</strong>
463
+ <button class="product-button clear-cart" onclick="clearCart()">Очистить</button>
464
  <button class="product-button order-button" onclick="orderViaWhatsApp()">Заказать</button>
465
  </div>
466
  </div>
 
470
 
471
  <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
472
  <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.5.3/dist/umd/popper.min.js"></script>
 
473
  <script src="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.js"></script>
474
  <script>
475
  const products = {{ products|tojson }};
476
  let selectedProductIndex = null;
477
 
478
+ function toggleTheme() {
479
+ document.body.classList.toggle('dark-mode');
480
+ const icon = document.querySelector('.theme-toggle i');
481
+ icon.classList.toggle('fa-moon');
482
+ icon.classList.toggle('fa-sun');
483
+ localStorage.setItem('theme', document.body.classList.contains('dark-mode') ? 'dark' : 'light');
484
+ }
485
+
486
+ if (localStorage.getItem('theme') === 'dark') {
487
+ document.body.classList.add('dark-mode');
488
+ document.querySelector('.theme-toggle i').classList.replace('fa-moon', 'fa-sun');
489
+ }
490
+
491
  function openModal(index) {
 
492
  loadProductDetails(index);
493
  document.getElementById('productModal').style.display = "block";
494
  }
495
 
496
  function closeModal(modalId) {
 
497
  document.getElementById(modalId).style.display = "none";
498
  }
499
 
 
504
  document.getElementById('modalContent').innerHTML = data;
505
  initializeSwiper();
506
  })
507
+ .catch(error => console.error('Ошибка:', error));
508
  }
509
 
510
  function initializeSwiper() {
 
513
  spaceBetween: 20,
514
  loop: true,
515
  grabCursor: true,
516
+ pagination: { el: '.swiper-pagination', clickable: true },
517
+ navigation: { nextEl: '.swiper-button-next', prevEl: '.swiper-button-prev' },
518
+ zoom: { maxRatio: 3 }
 
 
 
 
 
 
 
 
519
  });
520
  }
521
 
522
  function openQuantityModal(index) {
 
523
  selectedProductIndex = index;
524
  document.getElementById('quantityModal').style.display = 'block';
525
  document.getElementById('quantityInput').value = 1;
526
  }
527
 
528
  function confirmAddToCart() {
529
+ if (selectedProductIndex === null) return;
530
+ const quantity = parseInt(document.getElementById('quantityInput').value) || 1;
 
 
 
 
 
 
 
 
531
  if (quantity <= 0) {
532
+ alert("Укажите количество больше 0");
 
533
  return;
534
  }
 
535
  let cart = JSON.parse(localStorage.getItem('cart') || '[]');
536
  const product = products[selectedProductIndex];
 
 
 
 
 
 
 
537
  const existingItem = cart.find(item => item.name === product.name);
538
 
539
  if (existingItem) {
540
  existingItem.quantity += quantity;
 
541
  } else {
542
+ cart.push({
543
  name: product.name,
544
  price: product.price,
545
  photo: product.photos && product.photos.length > 0 ? product.photos[0] : '',
546
  quantity: quantity
547
+ });
 
 
548
  }
549
 
550
  localStorage.setItem('cart', JSON.stringify(cart));
 
551
  closeModal('quantityModal');
552
  updateCartButton();
553
  }
554
 
555
  function updateCartButton() {
556
  const cart = JSON.parse(localStorage.getItem('cart') || '[]');
557
+ document.getElementById('cart-button').style.display = cart.length > 0 ? 'block' : 'none';
 
 
558
  }
559
 
560
  function openCartModal() {
 
561
  const cart = JSON.parse(localStorage.getItem('cart') || '[]');
562
  const cartContent = document.getElementById('cartContent');
563
  let total = 0;
564
 
565
+ cartContent.innerHTML = cart.length === 0 ? '<p>Корзина пуста</p>' : cart.map(item => {
566
+ const itemTotal = item.price * item.quantity;
567
+ total += itemTotal;
568
+ return `
569
+ <div class="cart-item">
570
+ <div style="display: flex; align-items: center;">
571
+ ${item.photo ? `<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/${item.photo}" alt="${item.name}">` : ''}
572
+ <div>
573
+ <strong>${item.name}</strong>
574
+ <p>${item.price} × ${item.quantity}</p>
 
 
 
 
 
 
 
575
  </div>
 
576
  </div>
577
+ <span>${itemTotal} ₽</span>
578
+ </div>
579
+ `;
580
+ }).join('');
581
 
582
  document.getElementById('cartTotal').textContent = total;
583
  document.getElementById('cartModal').style.display = 'block';
 
586
  function orderViaWhatsApp() {
587
  const cart = JSON.parse(localStorage.getItem('cart') || '[]');
588
  if (cart.length === 0) {
589
+ alert("Корзина пуста!");
590
  return;
591
  }
 
592
  let total = 0;
593
  let orderText = "Заказ:%0A";
594
  cart.forEach((item, index) => {
595
  const itemTotal = item.price * item.quantity;
596
  total += itemTotal;
597
+ orderText += `${index + 1}. ${item.name} - ${item.price} × ${item.quantity}%0A`;
598
  });
599
+ orderText += `Итого: ${total} ₽`;
600
+ window.open(`https://api.whatsapp.com/send?phone=996500398754&text=${orderText}`, '_blank');
 
 
 
601
  }
602
 
603
  function clearCart() {
 
604
  localStorage.removeItem('cart');
605
  closeModal('cartModal');
606
  updateCartButton();
607
  }
608
 
609
  window.onclick = function(event) {
610
+ if (event.target.className === 'modal') event.target.style.display = "none";
 
 
 
611
  }
612
 
613
+ document.getElementById('search-input').addEventListener('input', filterProducts);
614
+ document.querySelectorAll('.category-filter').forEach(filter => {
 
 
 
 
 
615
  filter.addEventListener('click', function() {
616
+ document.querySelectorAll('.category-filter').forEach(f => f.classList.remove('active'));
617
  this.classList.add('active');
618
  filterProducts();
619
  });
 
622
  function filterProducts() {
623
  const searchTerm = document.getElementById('search-input').value.toLowerCase();
624
  const activeCategory = document.querySelector('.category-filter.active').dataset.category;
625
+ document.querySelectorAll('.product').forEach(product => {
 
 
626
  const name = product.getAttribute('data-name');
627
  const description = product.getAttribute('data-description');
628
  const category = product.getAttribute('data-category');
 
629
  const matchesSearch = name.includes(searchTerm) || description.includes(searchTerm);
630
  const matchesCategory = activeCategory === 'all' || category === activeCategory;
631
+ product.style.display = matchesSearch && matchesCategory ? 'block' : 'none';
 
 
 
 
 
632
  });
633
  }
634
 
 
635
  updateCartButton();
636
  </script>
637
  </body>
 
648
  except IndexError:
649
  return "Продукт не найден", 404
650
  detail_html = '''
651
+ <div class="container" style="padding: 20px;">
652
+ <h2 style="font-size: 1.8rem; font-weight: 600; margin-bottom: 20px;">{{ product['name'] }}</h2>
653
+ <div class="swiper-container" style="max-width: 400px; margin: 0 auto 20px;">
654
  <div class="swiper-wrapper">
655
  {% if product.get('photos') %}
656
  {% for photo in product['photos'] %}
 
658
  <div class="swiper-zoom-container">
659
  <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo }}"
660
  alt="{{ product['name'] }}"
661
+ style="max-width: 100%; max-height: 300px; object-fit: contain;">
662
  </div>
663
  </div>
664
  {% endfor %}
665
  {% else %}
666
  <div class="swiper-slide">
667
+ <img src="https://via.placeholder.com/300" alt="No Image">
668
  </div>
669
  {% endif %}
670
  </div>
 
673
  <div class="swiper-button-prev"></div>
674
  </div>
675
  <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
676
+ <p><strong>Цена:</strong> {{ product['price'] }} ₽</p>
677
  <p><strong>Описание:</strong> {{ product['description'] }}</p>
678
  </div>
679
  '''
 
688
  if request.method == 'POST':
689
  action = request.form.get('action')
690
 
 
691
  if action == 'add_category':
692
  category_name = request.form.get('category_name')
693
  if category_name and category_name not in categories:
694
  categories.append(category_name)
695
  save_data(data)
 
696
  return redirect(url_for('admin'))
697
+ return "Ошибка: Категория уже существует или не указано название", 400
 
698
 
699
  elif action == 'delete_category':
700
  category_index = int(request.form.get('category_index'))
701
  deleted_category = categories.pop(category_index)
 
702
  for product in products:
703
  if product.get('category') == deleted_category:
704
  product['category'] = 'Без категории'
705
  save_data(data)
 
706
  return redirect(url_for('admin'))
707
 
 
708
  elif action == 'add':
709
  name = request.form.get('name')
710
  price = request.form.get('price')
 
713
  photos_files = request.files.getlist('photos')
714
  photos_list = []
715
 
 
 
716
  if photos_files:
717
+ for photo in photos_files[:2]:
718
  if photo and photo.filename:
719
  photo_filename = secure_filename(photo.filename)
720
  uploads_dir = 'uploads'
721
  os.makedirs(uploads_dir, exist_ok=True)
722
  temp_path = os.path.join(uploads_dir, photo_filename)
723
  photo.save(temp_path)
724
+ api = HfApi()
725
+ api.upload_file(
726
+ path_or_fileobj=temp_path,
727
+ path_in_repo=f"photos/{photo_filename}",
728
+ repo_id=REPO_ID,
729
+ repo_type="dataset",
730
+ token=HF_TOKEN_WRITE,
731
+ commit_message=f"Добавлено фото для товара {name}"
732
+ )
733
+ photos_list.append(photo_filename)
734
+ if os.path.exists(temp_path):
735
+ os.remove(temp_path)
 
 
 
 
 
 
736
 
737
  if not name or not price or not description:
 
738
  return "Ошибка: Заполните все обязательные поля", 400
739
 
740
+ price = float(price.replace(',', '.'))
 
 
 
 
 
741
  new_product = {
742
  'name': name,
743
  'price': price,
 
745
  'category': category if category in categories else 'Без категории',
746
  'photos': photos_list
747
  }
 
748
  products.append(new_product)
749
  save_data(data)
 
750
  return redirect(url_for('admin'))
751
 
752
  elif action == 'edit':
 
757
  category = request.form.get('category')
758
  photos_files = request.files.getlist('photos')
759
 
 
 
760
  if photos_files and any(photo.filename for photo in photos_files):
761
  new_photos_list = []
762
+ for photo in photos_files[:2]:
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
  save_data(data)
 
788
  return redirect(url_for('admin'))
789
 
790
  elif action == 'delete':
791
  index = int(request.form.get('index'))
 
792
  del products[index]
793
  save_data(data)
 
794
  return redirect(url_for('admin'))
795
 
796
  admin_html = '''
 
800
  <meta charset="UTF-8">
801
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
802
  <title>Админ-панель</title>
803
+ <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
804
  <style>
805
  body {
806
+ font-family: 'Poppins', sans-serif;
807
+ background: linear-gradient(135deg, #f0f2f5, #e9ecef);
808
+ color: #2d3748;
809
+ padding: 20px;
810
+ }
811
+ .container {
812
+ max-width: 1200px;
813
+ margin: 0 auto;
814
  }
815
  h1, h2 {
816
+ font-weight: 600;
817
+ margin-bottom: 20px;
818
  }
819
  form {
820
+ background: #fff;
821
+ padding: 20px;
822
+ border-radius: 15px;
823
+ box-shadow: 0 4px 15px rgba(0,0,0,0.1);
824
+ margin-bottom: 30px;
 
825
  }
826
  label {
827
+ font-weight: 500;
828
+ margin-top: 15px;
829
  display: block;
 
 
830
  }
831
  input, textarea, select {
832
  width: 100%;
833
+ padding: 12px;
834
  margin-top: 5px;
835
+ border: 1px solid #e2e8f0;
836
+ border-radius: 10px;
837
+ font-size: 1rem;
838
+ transition: all 0.3s ease;
839
+ }
840
+ input:focus, textarea:focus, select:focus {
841
+ border-color: #2b6cb0;
842
+ box-shadow: 0 0 5px rgba(43, 108, 176, 0.3);
843
+ outline: none;
844
  }
845
  button {
846
+ padding: 12px 20px;
 
 
 
847
  border: none;
848
+ border-radius: 25px;
849
+ background-color: #2b6cb0;
850
+ color: white;
851
+ font-weight: 500;
852
  cursor: pointer;
853
+ transition: all 0.3s ease;
854
+ margin-top: 15px;
855
  }
856
  button:hover {
857
+ background-color: #2c5282;
858
+ box-shadow: 0 2px 10px rgba(44, 82, 130, 0.4);
859
  }
860
  .delete-button {
861
+ background-color: #e53e3e;
862
  }
863
  .delete-button:hover {
864
+ background-color: #c53030;
865
  }
866
  .product-list, .category-list {
867
+ display: grid;
868
+ gap: 20px;
869
  }
870
  .product-item, .category-item {
871
+ background: #fff;
872
+ padding: 20px;
873
+ border-radius: 15px;
874
+ box-shadow: 0 4px 15px rgba(0,0,0,0.1);
 
875
  }
876
  .edit-form {
877
+ margin-top: 15px;
878
+ padding: 15px;
879
+ background: #f7fafc;
880
+ border-radius: 10px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
881
  }
882
  </style>
883
  </head>
884
  <body>
885
+ <div class="container">
886
+ <h1>Добавление товара</h1>
887
+ <form method="POST" enctype="multipart/form-data">
888
+ <input type="hidden" name="action" value="add">
889
+ <label>Название товара:</label>
890
+ <input type="text" name="name" required>
891
+ <label>Цена:</label>
892
+ <input type="number" name="price" step="0.01" required>
893
+ <label>Описание:</label>
894
+ <textarea name="description" rows="4" required></textarea>
895
+ <label>Категория:</label>
896
+ <select name="category">
897
+ <option value="Без категории">Без категории</option>
898
+ {% for category in categories %}
899
+ <option value="{{ category }}">{{ category }}</option>
900
+ {% endfor %}
901
+ </select>
902
+ <label>Фотографии (до 2):</label>
903
+ <input type="file" name="photos" accept="image/*" multiple>
904
+ <button type="submit">Добавить</button>
905
+ </form>
906
+
907
+ <h1>Управление категориями</h1>
908
+ <form method="POST">
909
+ <input type="hidden" name="action" value="add_category">
910
+ <label>Название категории:</label>
911
+ <input type="text" name="category_name" required>
912
+ <button type="submit">Добавить</button>
913
+ </form>
914
+
915
+ <h2>Список категорий</h2>
916
+ <div class="category-list">
917
  {% for category in categories %}
918
+ <div class="category-item">
919
+ <h3>{{ category }}</h3>
920
+ <form method="POST" style="display: inline;">
921
+ <input type="hidden" name="action" value="delete_category">
922
+ <input type="hidden" name="category_index" value="{{ loop.index0 }}">
923
+ <button type="submit" class="delete-button">Удалить</button>
924
+ </form>
925
+ </div>
926
  {% endfor %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
927
  </div>
 
 
928
 
929
+ <h2>Управление базой данных</h2>
930
+ <form method="POST" action="{{ url_for('backup') }}" style="display: inline;">
931
+ <button type="submit">Создать копию</button>
932
+ </form>
933
+ <form method="GET" action="{{ url_for('download') }}" style="display: inline;">
934
+ <button type="submit">Скачать базу</button>
935
+ </form>
936
 
937
+ <h2>Список товаров</h2>
938
+ <div class="product-list">
939
+ {% for product in products %}
940
+ <div class="product-item">
941
+ <h3>{{ product['name'] }}</h3>
942
+ <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
943
+ <p><strong>Цена:</strong> {{ product['price'] }} ₽</p>
944
+ <p><strong>Описание:</strong> {{ product['description'] }}</p>
945
+ {% if product.get('photos') and product['photos']|length > 0 %}
946
+ <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product['photos'][0] }}"
947
+ alt="{{ product['name'] }}"
948
+ style="max-width: 100px; border-radius: 10px;">
949
+ {% endif %}
950
+ <details>
951
+ <summary>Редактировать</summary>
952
+ <form method="POST" enctype="multipart/form-data" class="edit-form">
953
+ <input type="hidden" name="action" value="edit">
954
+ <input type="hidden" name="index" value="{{ loop.index0 }}">
955
+ <label>Название:</label>
956
+ <input type="text" name="name" value="{{ product['name'] }}" required>
957
+ <label>Цена:</label>
958
+ <input type="number" name="price" step="0.01" value="{{ product['price'] }}" required>
959
+ <label>Описание:</label>
960
+ <textarea name="description" rows="4" required>{{ product['description'] }}</textarea>
961
+ <label>Категория:</label>
962
+ <select name="category">
963
+ <option value="Без категории" {% if product.get('category', 'Без категории') == 'Без категории' %}selected{% endif %}>Без категории</option>
964
+ {% for category in categories %}
965
+ <option value="{{ category }}" {% if product.get('category') == category %}selected{% endif %}>{{ category }}</option>
966
+ {% endfor %}
967
+ </select>
968
+ <label>Фотографии:</label>
969
+ <input type="file" name="photos" accept="image/*" multiple>
970
+ <button type="submit">Сохранить</button>
971
+ </form>
972
+ </details>
973
+ <form method="POST">
974
+ <input type="hidden" name="action" value="delete">
975
  <input type="hidden" name="index" value="{{ loop.index0 }}">
976
+ <button type="submit" class="delete-button">Удалить</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
977
  </form>
978
+ </div>
979
+ {% endfor %}
 
 
 
 
980
  </div>
 
981
  </div>
982
  </body>
983
  </html>
 
987
  @app.route('/backup', methods=['POST'])
988
  def backup():
989
  upload_db_to_hf()
990
+ return "Резервная копия создана.", 200
991
 
992
  @app.route('/download', methods=['GET'])
993
  def download():
994
  download_db_from_hf()
995
+ return "База данных скачана.", 200
996
 
997
  if __name__ == '__main__':
998
  backup_thread = threading.Thread(target=periodic_backup, daemon=True)
999
  backup_thread.start()
 
1000
  try:
1001
  load_data()
1002
  except Exception as e:
1003
+ logging.error(f"Не удалось загрузить базу данных: {e}")
 
1004
  app.run(debug=True, host='0.0.0.0', port=7860)