flpolprojects commited on
Commit
98a25c5
·
verified ·
1 Parent(s): 38eda2f

Update app.py

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