Kgshop commited on
Commit
a69332c
·
verified ·
1 Parent(s): e87adc4

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +196 -400
app.py CHANGED
@@ -1,4 +1,5 @@
1
  # --- START OF FILE app.py ---
 
2
  from flask import Flask, render_template_string, request, redirect, url_for
3
  import json
4
  import os
@@ -19,7 +20,7 @@ HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
19
 
20
  LOGO_URL = "https://huggingface.co/spaces/Teenagerkg/optom/resolve/main/PXL_20250419_062456154~2.jpg"
21
 
22
- logging.basicConfig(level=logging.INFO)
23
 
24
  def load_data():
25
  try:
@@ -54,9 +55,6 @@ def save_data(data):
54
  raise
55
 
56
  def upload_db_to_hf():
57
- if not HF_TOKEN_WRITE:
58
- logging.warning("HF_TOKEN_WRITE не установлен. Пропуск загрузки на Hugging Face.")
59
- return
60
  try:
61
  api = HfApi()
62
  api.upload_file(
@@ -72,14 +70,6 @@ def upload_db_to_hf():
72
  logging.error(f"Ошибка при загрузке резервной копии: {e}")
73
 
74
  def download_db_from_hf():
75
- if not HF_TOKEN_READ:
76
- logging.warning("HF_TOKEN_READ не установлен. Пропуск скачивания с Hugging Face.")
77
- # If read token is missing, try to use write token if available
78
- if HF_TOKEN_WRITE:
79
- HF_TOKEN_READ = HF_TOKEN_WRITE
80
- logging.info("Используется HF_TOKEN_WRITE для скачивания.")
81
- else:
82
- raise Exception("Нет доступных токенов для чтения.")
83
  try:
84
  hf_hub_download(
85
  repo_id=REPO_ID,
@@ -99,16 +89,14 @@ def download_db_from_hf():
99
 
100
  def periodic_backup():
101
  while True:
102
- time.sleep(800)
103
- logging.info("Запуск периодического резервного копирования.")
104
  upload_db_to_hf()
105
-
106
 
107
  @app.route('/')
108
  def catalog():
109
  data = load_data()
110
- products = sorted(data.get('products', []), key=lambda x: x.get('added_at', ''), reverse=True)
111
- categories = data.get('categories', [])
112
 
113
  catalog_html = '''
114
  <!DOCTYPE html>
@@ -219,19 +207,9 @@ def catalog():
219
  }
220
  .products-grid {
221
  display: grid;
222
- grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
223
  gap: 15px;
224
  padding: 10px;
225
- }
226
- @media (min-width: 768px) {
227
- .products-grid {
228
- grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
229
- }
230
- }
231
- @media (min-width: 1024px) {
232
- .products-grid {
233
- grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
234
- }
235
  }
236
  .product {
237
  background: var(--light-text);
@@ -240,8 +218,6 @@ def catalog():
240
  box-shadow: 0 4px 15px var(--shadow-color);
241
  transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.3s ease;
242
  overflow: hidden;
243
- display: flex;
244
- flex-direction: column;
245
  }
246
  .product:hover {
247
  transform: translateY(-5px) scale(1.02);
@@ -249,14 +225,13 @@ def catalog():
249
  }
250
  .product-image {
251
  width: 100%;
252
- aspect-ratio: 1 / 1;
253
  background-color: #fff;
254
  border-radius: 10px;
255
  overflow: hidden;
256
  display: flex;
257
  justify-content: center;
258
  align-items: center;
259
- margin-bottom: 10px;
260
  }
261
  .product-image img {
262
  max-width: 100%;
@@ -270,12 +245,11 @@ def catalog():
270
  .product h2 {
271
  font-size: 1rem;
272
  font-weight: 600;
273
- margin: 10px 0 5px 0;
274
  text-align: center;
275
  white-space: nowrap;
276
  overflow: hidden;
277
  text-overflow: ellipsis;
278
- flex-grow: 0; /* Prevent title from growing */
279
  }
280
  .product-price {
281
  font-size: 1.1rem;
@@ -283,7 +257,6 @@ def catalog():
283
  font-weight: 700;
284
  text-align: center;
285
  margin: 5px 0;
286
- flex-grow: 0;
287
  }
288
  .product-description {
289
  font-size: 0.8rem;
@@ -293,14 +266,6 @@ def catalog():
293
  overflow: hidden;
294
  text-overflow: ellipsis;
295
  white-space: nowrap;
296
- flex-grow: 1; /* Allow description to take space if needed */
297
- min-height: 1.2em; /* Ensure minimum height */
298
- }
299
- .product-buttons {
300
- margin-top: auto; /* Push buttons to bottom */
301
- display: flex;
302
- flex-direction: column;
303
- gap: 5px;
304
  }
305
  .product-button {
306
  display: block;
@@ -314,6 +279,7 @@ def catalog():
314
  font-weight: 500;
315
  cursor: pointer;
316
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
 
317
  text-align: center;
318
  text-decoration: none;
319
  }
@@ -342,8 +308,6 @@ def catalog():
342
  font-size: 1.2rem;
343
  cursor: pointer;
344
  display: none;
345
- align-items: center;
346
- justify-content: center;
347
  box-shadow: 0 4px 15px rgba(255, 167, 38, 0.4);
348
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
349
  z-index: 1000;
@@ -361,7 +325,6 @@ def catalog():
361
  height: 100%;
362
  background-color: rgba(0,0,0,0.5);
363
  backdrop-filter: blur(5px);
364
- overflow-y: auto; /* Allow modal itself to scroll if content is very tall */
365
  }
366
  .modal-content {
367
  background: var(--light-text);
@@ -372,23 +335,29 @@ def catalog():
372
  max-width: 700px;
373
  box-shadow: 0 10px 30px rgba(0,0,0,0.2);
374
  animation: slideIn 0.3s ease-out;
375
- position: relative; /* Needed for close button positioning */
376
- max-height: 85vh; /* Limit modal height */
377
- overflow-y: auto; /* Enable scrolling WITHIN the modal content area */
 
 
 
 
 
 
 
 
378
  }
379
  @keyframes slideIn {
380
  from { transform: translateY(-50px); opacity: 0; }
381
  to { transform: translateY(0); opacity: 1; }
382
  }
383
  .close {
384
- position: absolute; /* Position relative to modal-content */
385
- top: 10px;
386
- right: 15px;
387
- font-size: 1.8rem; /* Make close button slightly larger */
388
- color: #aaa;
389
  cursor: pointer;
390
  transition: color 0.3s;
391
- line-height: 1; /* Ensure consistent vertical alignment */
392
  }
393
  .close:hover {
394
  color: var(--primary-color);
@@ -399,55 +368,13 @@ def catalog():
399
  align-items: center;
400
  padding: 15px 0;
401
  border-bottom: 1px solid var(--secondary-color);
402
- gap: 10px; /* Add gap */
403
- }
404
- .cart-item-details {
405
- display: flex;
406
- align-items: center;
407
- gap: 15px;
408
- flex-grow: 1; /* Allow details to take space */
409
- min-width: 0; /* Prevent overflow issues */
410
  }
411
  .cart-item img {
412
  width: 50px;
413
  height: 50px;
414
  object-fit: contain;
415
  border-radius: 8px;
416
- flex-shrink: 0; /* Prevent image shrinking */
417
- }
418
- .cart-item-info {
419
- min-width: 0; /* Prevent overflow */
420
- }
421
- .cart-item-info strong {
422
- display: block; /* Ensure name is on its own line */
423
- white-space: nowrap;
424
- overflow: hidden;
425
- text-overflow: ellipsis;
426
- }
427
- .cart-item-info p {
428
- font-size: 0.9em;
429
- color: #555;
430
- white-space: nowrap;
431
- overflow: hidden;
432
- text-overflow: ellipsis;
433
- }
434
- .cart-item-price {
435
- font-weight: bold;
436
- white-space: nowrap; /* Prevent price breaking */
437
- flex-shrink: 0; /* Prevent price shrinking */
438
- }
439
- .cart-actions {
440
- margin-top: 20px;
441
- text-align: right;
442
- display: flex;
443
- flex-wrap: wrap; /* Allow buttons to wrap on small screens */
444
- justify-content: flex-end; /* Align buttons right */
445
- gap: 10px; /* Space between buttons */
446
- }
447
- .cart-actions strong {
448
- width: 100%; /* Make total take full width */
449
- margin-bottom: 10px; /* Space below total */
450
- font-size: 1.1rem;
451
  }
452
  .quantity-input, .color-select {
453
  width: 100%;
@@ -472,10 +399,16 @@ def catalog():
472
  background-color: var(--hover-secondary-color);
473
  box-shadow: 0 4px 15px rgba(255, 160, 0, 0.4);
474
  }
 
 
 
 
 
 
475
  @media (max-width: 768px) {
476
- .products-grid {
477
- grid-template-columns: repeat(2, minmax(150px, 1fr));
478
- }
479
  .header h1 {
480
  font-size: 1.2rem;
481
  }
@@ -506,13 +439,10 @@ def catalog():
506
  }
507
  .modal-content {
508
  padding: 15px;
509
- padding-top: 40px; /* Add padding top for absolute close button */
510
- max-height: 90vh; /* Allow slightly more height on mobile */
511
  }
512
- .close {
513
- top: 5px;
514
- right: 10px;
515
- font-size: 1.6rem;
516
  }
517
  }
518
  @media (max-width: 480px) {
@@ -536,26 +466,6 @@ def catalog():
536
  .category-filter {
537
  padding: 5px 10px;
538
  }
539
- .cart-item {
540
- flex-direction: column; /* Stack cart items vertically */
541
- align-items: flex-start; /* Align left */
542
- }
543
- .cart-item-details {
544
- width: 100%; /* Take full width */
545
- }
546
- .cart-item-price {
547
- align-self: flex-end; /* Move price to the right */
548
- margin-top: 5px;
549
- }
550
- .cart-actions {
551
- justify-content: center; /* Center buttons on small screens */
552
- }
553
- .cart-actions button {
554
- width: 100%; /* Make buttons full width */
555
- }
556
- }
557
- .product-hidden {
558
- display: none !important; /* Use !important to override grid display */
559
  }
560
  </style>
561
  </head>
@@ -582,22 +492,16 @@ def catalog():
582
  data-category="{{ product.get('category', 'Без категории') }}">
583
  {% if product.get('photos') and product['photos']|length > 0 %}
584
  <div class="product-image">
585
- <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product['photos'][0] }}?download=true"
586
  alt="{{ product['name'] }}"
587
  loading="lazy">
588
  </div>
589
- {% else %}
590
- <div class="product-image">
591
- <span style="color: #ccc; font-size: 0.9rem;">Нет фото</span>
592
- </div>
593
  {% endif %}
594
  <h2>{{ product['name'] }}</h2>
595
  <div class="product-price">{{ product['price'] }} с</div>
596
- <p class="product-description" title="{{ product['description'] }}">{{ product['description'][:50] }}{% if product['description']|length > 50 %}...{% endif %}</p>
597
- <div class="product-buttons">
598
- <button class="product-button" onclick="openModal({{ loop.index0 }})">Подробнее</button>
599
- <button class="product-button add-to-cart" onclick="openQuantityModal({{ loop.index0 }})">В корзину</button>
600
- </div>
601
  </div>
602
  {% endfor %}
603
  </div>
@@ -614,8 +518,8 @@ def catalog():
614
  <div class="modal-content">
615
  <span class="close" onclick="closeModal('quantityModal')">×</span>
616
  <h2>Укажите количество и цвет</h2>
617
- <input type="number" id="quantityInput" class="quantity-input" min="1" value="1" style="margin-bottom: 15px;">
618
- <select id="colorSelect" class="color-select" style="margin-bottom: 15px;"></select>
619
  <button class="product-button" onclick="confirmAddToCart()">Добавить</button>
620
  </div>
621
  </div>
@@ -625,7 +529,7 @@ def catalog():
625
  <span class="close" onclick="closeModal('cartModal')">×</span>
626
  <h2>Корзина</h2>
627
  <div id="cartContent"></div>
628
- <div class="cart-actions">
629
  <strong>Итого: <span id="cartTotal">0</span> с</strong>
630
  <button class="product-button clear-cart" onclick="clearCart()">Очистить</button>
631
  <button class="product-button order-button" onclick="orderViaWhatsApp()">Заказать</button>
@@ -635,6 +539,8 @@ def catalog():
635
 
636
  <button id="cart-button" onclick="openCartModal()"><i class="fas fa-shopping-cart"></i></button>
637
 
 
 
638
  <script src="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.js"></script>
639
  <script>
640
  const products = {{ products|tojson }};
@@ -643,45 +549,32 @@ def catalog():
643
  function openModal(index) {
644
  loadProductDetails(index);
645
  document.getElementById('productModal').style.display = "block";
646
- document.body.style.overflow = 'hidden'; // Prevent background scroll
647
  }
648
 
649
  function closeModal(modalId) {
650
  document.getElementById(modalId).style.display = "none";
651
- document.body.style.overflow = ''; // Restore background scroll
652
  }
653
 
654
  function loadProductDetails(index) {
655
  fetch('/product/' + index)
656
- .then(response => {
657
- if (!response.ok) throw new Error('Network response was not ok');
658
- return response.text();
659
- })
660
  .then(data => {
661
  document.getElementById('modalContent').innerHTML = data;
662
  initializeSwiper();
663
  })
664
- .catch(error => {
665
- console.error('Ошибка загрузки деталей продукта:', error);
666
- document.getElementById('modalContent').innerHTML = '<p>Не удалось загрузить информацию о товаре.</p>';
667
- });
668
  }
669
 
670
  function initializeSwiper() {
671
- // Check if Swiper element exists before initializing
672
- if (document.querySelector('.swiper-container')) {
673
- new Swiper('.swiper-container', {
674
- slidesPerView: 1,
675
- spaceBetween: 20,
676
- loop: true,
677
- grabCursor: true,
678
- pagination: { el: '.swiper-pagination', clickable: true },
679
- navigation: { nextEl: '.swiper-button-next', prevEl: '.swiper-button-prev' },
680
- zoom: { maxRatio: 3 },
681
- observer: true, // Reinitialize Swiper when slides or container change
682
- observeParents: true // Reinitialize Swiper on parent changes
683
- });
684
- }
685
  }
686
 
687
  function openQuantityModal(index) {
@@ -689,18 +582,21 @@ def catalog():
689
  const product = products[index];
690
  const colorSelect = document.getElementById('colorSelect');
691
  colorSelect.innerHTML = '';
692
- const colors = product.colors && Array.isArray(product.colors) && product.colors.length > 0 ? product.colors : ['Нет цвета'];
693
-
694
- colors.forEach(color => {
 
 
 
 
 
695
  const option = document.createElement('option');
696
- option.value = color;
697
- option.text = color;
698
  colorSelect.appendChild(option);
699
- });
700
-
701
  document.getElementById('quantityModal').style.display = 'block';
702
  document.getElementById('quantityInput').value = 1;
703
- document.body.style.overflow = 'hidden';
704
  }
705
 
706
  function confirmAddToCart() {
@@ -714,18 +610,16 @@ def catalog():
714
  let cart = JSON.parse(localStorage.getItem('cart') || '[]');
715
  const product = products[selectedProductIndex];
716
  const cartItemId = `${product.name}-${color}`;
717
- const existingItemIndex = cart.findIndex(item => item.id === cartItemId);
718
 
719
- if (existingItemIndex > -1) {
720
- cart[existingItemIndex].quantity += quantity;
721
  } else {
722
- const photo = product.photos && product.photos.length > 0 ? product.photos[0] : '';
723
- const price = typeof product.price === 'number' ? product.price : parseFloat(String(product.price).replace(',', '.')) || 0;
724
  cart.push({
725
  id: cartItemId,
726
  name: product.name,
727
- price: price,
728
- photo: photo,
729
  quantity: quantity,
730
  color: color
731
  });
@@ -740,6 +634,8 @@ def catalog():
740
  const cart = JSON.parse(localStorage.getItem('cart') || '[]');
741
  const cartButton = document.getElementById('cart-button');
742
  cartButton.style.display = cart.length > 0 ? 'flex' : 'none';
 
 
743
  }
744
 
745
  function openCartModal() {
@@ -748,29 +644,24 @@ def catalog():
748
  let total = 0;
749
 
750
  cartContent.innerHTML = cart.length === 0 ? '<p>Корзина пуста</p>' : cart.map(item => {
751
- const itemPrice = typeof item.price === 'number' ? item.price : 0;
752
- const itemQuantity = typeof item.quantity === 'number' ? item.quantity : 0;
753
- const itemTotal = itemPrice * itemQuantity;
754
  total += itemTotal;
755
- const photoUrl = item.photo ? `https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/${item.photo}?download=true` : '';
756
-
757
  return `
758
  <div class="cart-item">
759
- <div class="cart-item-details">
760
- ${item.photo ? `<img src="${photoUrl}" alt="${item.name}">` : '<div style="width:50px; height:50px; background:#eee; border-radius:8px; flex-shrink:0;"></div>'}
761
- <div class="cart-item-info">
762
  <strong>${item.name}</strong>
763
- <p>${itemPrice.toFixed(2)} с × ${itemQuantity} (Цвет: ${item.color})</p>
764
  </div>
765
  </div>
766
- <span class="cart-item-price">${itemTotal.toFixed(2)} с</span>
767
  </div>
768
  `;
769
  }).join('');
770
 
771
- document.getElementById('cartTotal').textContent = total.toFixed(2);
772
  document.getElementById('cartModal').style.display = 'block';
773
- document.body.style.overflow = 'hidden';
774
  }
775
 
776
  function orderViaWhatsApp() {
@@ -782,15 +673,12 @@ def catalog():
782
  let total = 0;
783
  let orderText = "Заказ:%0A";
784
  cart.forEach((item, index) => {
785
- const itemPrice = typeof item.price === 'number' ? item.price : 0;
786
- const itemQuantity = typeof item.quantity === 'number' ? item.quantity : 0;
787
- const itemTotal = itemPrice * itemQuantity;
788
  total += itemTotal;
789
- orderText += `${index + 1}. ${item.name} - ${itemPrice.toFixed(2)} с × ${itemQuantity} (Цвет: ${item.color})%0A`;
790
  });
791
- orderText += `Итого: ${total.toFixed(2)} с`;
792
- const whatsappUrl = `https://api.whatsapp.com/send?phone=996704667878&text=${orderText}`;
793
- window.open(whatsappUrl, '_blank');
794
  }
795
 
796
  function clearCart() {
@@ -801,7 +689,7 @@ def catalog():
801
 
802
  window.onclick = function(event) {
803
  if (event.target.classList.contains('modal')) {
804
- closeModal(event.target.id);
805
  }
806
  }
807
 
@@ -815,33 +703,19 @@ def catalog():
815
  });
816
 
817
  function filterProducts() {
818
- const searchTerm = document.getElementById('search-input').value.toLowerCase().trim();
819
  const activeCategory = document.querySelector('.category-filter.active').dataset.category;
820
- const grid = document.getElementById('products-grid');
821
- const products = grid.querySelectorAll('.product');
822
-
823
- products.forEach(product => {
824
- const name = product.getAttribute('data-name') || '';
825
- const description = product.getAttribute('data-description') || '';
826
- const category = product.getAttribute('data-category') || 'Без категории';
827
-
828
- const matchesSearch = searchTerm === '' || name.includes(searchTerm) || description.includes(searchTerm);
829
  const matchesCategory = activeCategory === 'all' || category === activeCategory;
830
-
831
- if (matchesSearch && matchesCategory) {
832
- product.classList.remove('product-hidden');
833
- product.style.display = ''; // Restore default grid display
834
- } else {
835
- product.classList.add('product-hidden');
836
- product.style.display = 'none'; // Explicitly hide
837
- }
838
  });
839
  }
840
 
841
- document.addEventListener('DOMContentLoaded', () => {
842
- updateCartButton();
843
- filterProducts(); // Initial filter application
844
- });
845
  </script>
846
  </body>
847
  </html>
@@ -851,42 +725,41 @@ def catalog():
851
  @app.route('/product/<int:index>')
852
  def product_detail(index):
853
  data = load_data()
854
- products = data.get('products', [])
855
- if not 0 <= index < len(products):
 
 
856
  return "Продукт не найден", 404
857
- product = products[index]
858
 
859
  detail_html = '''
860
- <div class="container" style="padding: 0; font-family: 'Poppins', sans-serif; color: #333;">
861
- <h2 style="font-size: 1.6rem; font-weight: 600; margin-bottom: 15px; color: #FFA726; text-align: center;">{{ product['name'] }}</h2>
862
- <div class="swiper-container" style="max-width: 450px; margin: 0 auto 20px; --swiper-navigation-color: #FFA726; --swiper-pagination-color: #FFA726; border-radius: 10px; overflow: hidden;">
863
  <div class="swiper-wrapper">
864
  {% if product.get('photos') and product['photos']|length > 0 %}
865
  {% for photo in product['photos'] %}
866
- <div class="swiper-slide" style="background-color: #fff; display: flex; justify-content: center; align-items: center; aspect-ratio: 1 / 1;">
867
  <div class="swiper-zoom-container">
868
- <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo }}?download=true"
869
  alt="{{ product['name'] }}"
870
- style="max-width: 100%; max-height: 100%; object-fit: contain;">
871
  </div>
872
  </div>
873
  {% endfor %}
874
  {% else %}
875
- <div class="swiper-slide" style="background-color: #eee; display: flex; justify-content: center; align-items: center; border-radius: 10px; aspect-ratio: 1 / 1;">
876
  <span style="color: #999;">Нет изображения</span>
877
  </div>
878
  {% endif %}
879
  </div>
880
- {% if product.get('photos') and product['photos']|length > 1 %}
881
- <div class="swiper-pagination" style="position: relative; bottom: 5px;"></div>
882
- <div class="swiper-button-next" style="right: 5px;"></div>
883
- <div class="swiper-button-prev" style="left: 5px;"></div>
884
- {% endif %}
885
  </div>
886
  <div style="background: #FFF8E1; padding: 15px; border-radius: 10px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
887
  <p style="margin-bottom: 10px;"><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
888
  <p style="margin-bottom: 10px;"><strong>Цена:</strong> <span style="color: #D84315; font-weight: bold;">{{ product['price'] }} с</span></p>
889
- <p style="margin-bottom: 10px;"><strong>Описание:</strong><br>{{ product['description'] | replace('\n', '<br>') | safe }}</p>
890
  <p><strong>Доступные цвета:</strong> {{ product.get('colors', ['Нет цветов'])|join(', ') if product.get('colors') else 'Нет цветов' }}</p>
891
  </div>
892
  </div>
@@ -897,68 +770,44 @@ def product_detail(index):
897
  @app.route('/admin', methods=['GET', 'POST'])
898
  def admin():
899
  data = load_data()
900
- products = data.get('products', [])
901
- categories = data.get('categories', [])
902
 
903
  if request.method == 'POST':
904
  action = request.form.get('action')
905
 
906
  if action == 'add_category':
907
- category_name = request.form.get('category_name', '').strip()
908
  if category_name and category_name not in categories:
909
  categories.append(category_name)
910
- save_data({'products': products, 'categories': categories})
911
- logging.info(f"Добавлена категория: {category_name}")
912
  return redirect(url_for('admin'))
913
- elif category_name in categories:
914
- return "Ошибка: Категория уже существует", 400
915
- else:
916
- return "Ошибка: Не указано название категории", 400
917
-
918
 
919
  elif action == 'delete_category':
920
- category_index_str = request.form.get('category_index')
921
- try:
922
- category_index = int(category_index_str)
923
- if 0 <= category_index < len(categories):
924
- deleted_category = categories.pop(category_index)
925
- for product in products:
926
- if product.get('category') == deleted_category:
927
- product['category'] = 'Без категории'
928
- save_data({'products': products, 'categories': categories})
929
- logging.info(f"Удалена категория: {deleted_category}")
930
- return redirect(url_for('admin'))
931
- else:
932
- return "Ошибка: Неверный индекс категории", 400
933
- except (ValueError, TypeError):
934
- return "Ошибка: Неверный индекс категории", 400
935
-
936
 
937
  elif action == 'add':
938
- name = request.form.get('name', '').strip()
939
- price_str = request.form.get('price', '').strip()
940
- description = request.form.get('description', '').strip()
941
  category = request.form.get('category')
942
  photos_files = request.files.getlist('photos')
943
  colors = [c.strip() for c in request.form.getlist('colors') if c.strip()]
944
  photos_list = []
945
 
946
- if not name or not price_str or not description:
947
- return "Ошибка: Заполните все обязательные поля (Название, Цена, Описание)", 400
948
-
949
- try:
950
- price = float(price_str.replace(',', '.'))
951
- if price < 0: raise ValueError("Цена не может быть отрицательной")
952
- except ValueError:
953
- return "Ошибка: Неверный формат цены", 400
954
-
955
- uploads_dir = 'uploads'
956
- os.makedirs(uploads_dir, exist_ok=True)
957
-
958
- if photos_files and HF_TOKEN_WRITE:
959
  for photo in photos_files[:10]:
960
  if photo and photo.filename:
961
  photo_filename = secure_filename(f"{int(time.time())}_{photo.filename}")
 
 
962
  temp_path = os.path.join(uploads_dir, photo_filename)
963
  try:
964
  photo.save(temp_path)
@@ -972,7 +821,6 @@ def admin():
972
  commit_message=f"Добавлено фото для товара {name}"
973
  )
974
  photos_list.append(photo_filename)
975
- logging.info(f"Загружено фото {photo_filename} для товара {name}")
976
  except Exception as e:
977
  logging.error(f"Ошибка при загрузке фото {photo_filename}: {e}")
978
  finally:
@@ -981,8 +829,15 @@ def admin():
981
  os.remove(temp_path)
982
  except OSError as e:
983
  logging.error(f"Ошибка при удалении временного файла {temp_path}: {e}")
984
- elif photos_files and not HF_TOKEN_WRITE:
985
- logging.warning("Пропуск загрузки фото: HF_TOKEN_WRITE не установлен.")
 
 
 
 
 
 
 
986
 
987
  new_product = {
988
  'name': name,
@@ -994,43 +849,40 @@ def admin():
994
  'added_at': datetime.now().isoformat()
995
  }
996
  products.append(new_product)
997
- save_data({'products': products, 'categories': categories})
998
- logging.info(f"Добавлен товар: {name}")
999
  return redirect(url_for('admin'))
1000
 
1001
  elif action == 'edit':
1002
- index_str = request.form.get('index')
1003
  try:
1004
- index = int(index_str)
1005
  if not 0 <= index < len(products):
1006
- return "Ошибка: Неверный индекс товара для редактирования", 400
1007
  except (ValueError, TypeError):
1008
- return "Ошибка: Неверный индекс товара для редактирования", 400
1009
 
1010
- name = request.form.get('name', '').strip()
1011
- price_str = request.form.get('price', '').strip()
1012
- description = request.form.get('description', '').strip()
1013
  category = request.form.get('category')
1014
  photos_files = request.files.getlist('photos')
1015
  colors = [c.strip() for c in request.form.getlist('colors') if c.strip()]
1016
 
1017
- if not name or not price_str or not description:
1018
- return "Ошибка: Заполните все обязательные поля (Название, Цена, Описание)", 400
1019
 
1020
  try:
1021
- price_float = float(price_str.replace(',', '.'))
1022
- if price_float < 0: raise ValueError("Цена не может быть отрицательной")
1023
  except ValueError:
1024
  return "Ошибка: Неверный формат цены", 400
1025
 
1026
- new_photos_list = []
1027
- uploads_dir = 'uploads'
1028
- os.makedirs(uploads_dir, exist_ok=True)
1029
 
1030
- if photos_files and any(photo.filename for photo in photos_files) and HF_TOKEN_WRITE:
 
1031
  for photo in photos_files[:10]:
1032
  if photo and photo.filename:
1033
  photo_filename = secure_filename(f"{int(time.time())}_{photo.filename}")
 
 
1034
  temp_path = os.path.join(uploads_dir, photo_filename)
1035
  try:
1036
  photo.save(temp_path)
@@ -1044,7 +896,6 @@ def admin():
1044
  commit_message=f"Обновлено фото для товара {name}"
1045
  )
1046
  new_photos_list.append(photo_filename)
1047
- logging.info(f"Загружено новое фото {photo_filename} для товара {name}")
1048
  except Exception as e:
1049
  logging.error(f"Ошибка при загрузке фото {photo_filename} при редактировании: {e}")
1050
  finally:
@@ -1053,36 +904,29 @@ def admin():
1053
  os.remove(temp_path)
1054
  except OSError as e:
1055
  logging.error(f"Ошибка при удалении временного файла {temp_path} при редактировании: {e}")
1056
- # TODO: Consider deleting old photos from HF if replaced. This needs tracking old filenames.
1057
- products[index]['photos'] = new_photos_list
1058
- elif photos_files and any(photo.filename for photo in photos_files) and not HF_TOKEN_WRITE:
1059
- logging.warning("Пропуск загрузки новых фото при редактировании: HF_TOKEN_WRITE не установлен.")
1060
 
1061
 
 
 
1062
  products[index]['name'] = name
1063
  products[index]['price'] = price_float
1064
  products[index]['description'] = description
1065
  products[index]['category'] = category if category in categories else 'Без категории'
1066
  products[index]['colors'] = colors
1067
 
1068
- save_data({'products': products, 'categories': categories})
1069
- logging.info(f"Отредактирован товар: {name} (индекс {index})")
1070
  return redirect(url_for('admin'))
1071
 
1072
  elif action == 'delete':
1073
- index_str = request.form.get('index')
1074
  try:
1075
- index = int(index_str)
1076
  if not 0 <= index < len(products):
1077
- return "Ошибка: Неверный индекс товара для удаления", 400
1078
 
1079
- deleted_product_name = products[index]['name']
1080
- # TODO: Consider deleting photos from HF associated with this product.
1081
  del products[index]
1082
- save_data({'products': products, 'categories': categories})
1083
- logging.info(f"Удален товар: {deleted_product_name} (индекс {index})")
1084
  except (ValueError, TypeError):
1085
- return "Ошибка: Неверный индекс товара для удаления", 400
1086
  except Exception as e:
1087
  logging.error(f"Ошибка при удалении товара: {e}")
1088
  return "Ошибка при удалении товара", 500
@@ -1193,7 +1037,8 @@ def admin():
1193
  padding: 10px;
1194
  background-color: #fff;
1195
  }
1196
- input[type="file"]::file-selector-button {
 
1197
  padding: 8px 15px;
1198
  border: none;
1199
  border-radius: 6px;
@@ -1221,9 +1066,6 @@ def admin():
1221
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
1222
  margin-top: 20px;
1223
  margin-right: 10px;
1224
- display: inline-flex; /* Align icon and text */
1225
- align-items: center;
1226
- gap: 8px; /* Space between icon and text */
1227
  }
1228
  button[type="submit"]:hover, button[type="button"]:hover {
1229
  background-color: var(--hover-primary-color);
@@ -1264,14 +1106,11 @@ def admin():
1264
  display: flex;
1265
  justify-content: space-between;
1266
  align-items: center;
1267
- flex-wrap: wrap; /* Allow wrapping */
1268
- gap: 10px; /* Add gap */
1269
  }
1270
  .category-item h3 {
1271
  margin: 0;
1272
  color: var(--text-color);
1273
  font-weight: 500;
1274
- flex-grow: 1; /* Allow title to take space */
1275
  }
1276
  .category-item form {
1277
  background: none;
@@ -1292,7 +1131,6 @@ def admin():
1292
  margin-bottom: 8px;
1293
  font-size: 0.9rem;
1294
  line-height: 1.5;
1295
- word-break: break-word; /* Prevent long descriptions from overflowing */
1296
  }
1297
  .product-item p strong {
1298
  color: #555;
@@ -1320,19 +1158,8 @@ def admin():
1320
  color: var(--primary-color);
1321
  padding: 8px 0;
1322
  outline: none;
1323
- display: inline-flex; /* Align icon and text */
1324
- align-items: center;
1325
- gap: 8px; /* Space between icon and text */
1326
  }
1327
- .product-item summary::marker {
1328
- content: ''; /* Remove default marker */
1329
- }
1330
- .product-item summary .fas {
1331
- transition: transform 0.3s ease;
1332
- }
1333
- .product-item details[open] summary .fas {
1334
- transform: rotate(90deg);
1335
- }
1336
  .product-item summary:hover {
1337
  color: var(--hover-primary-color);
1338
  }
@@ -1358,20 +1185,15 @@ def admin():
1358
  flex-grow: 1;
1359
  }
1360
  .color-input-group .remove-color-btn {
1361
- padding: 0; /* Remove padding */
1362
  font-size: 0.8rem;
1363
  background-color: #ddd;
1364
  color: #333;
1365
  border-radius: 50%;
1366
  margin-top: 0;
1367
- width: 25px; /* Fixed size */
1368
  height: 25px;
1369
- line-height: 25px; /* Center icon vertically */
1370
- text-align: center; /* Center icon horizontally */
1371
- flex-shrink: 0; /* Prevent shrinking */
1372
- display: inline-flex; /* Use flex to center icon */
1373
- align-items: center;
1374
- justify-content: center;
1375
  }
1376
  .color-input-group .remove-color-btn:hover {
1377
  background-color: #ccc;
@@ -1382,7 +1204,6 @@ def admin():
1382
  .button-group {
1383
  margin-top: 20px;
1384
  display: flex;
1385
- flex-wrap: wrap; /* Allow buttons to wrap */
1386
  gap: 10px;
1387
  }
1388
  .button-group form {
@@ -1390,9 +1211,8 @@ def admin():
1390
  padding: 0;
1391
  margin: 0;
1392
  box-shadow: none;
1393
- display: inline-block; /* Allow forms to sit side-by-side */
1394
  }
1395
- .button-group button, .button-group details {
1396
  margin-top: 0;
1397
  }
1398
 
@@ -1410,20 +1230,6 @@ def admin():
1410
  form {
1411
  padding: 20px;
1412
  }
1413
- .button-group {
1414
- flex-direction: column; /* Stack buttons vertically */
1415
- align-items: flex-start; /* Align buttons left */
1416
- }
1417
- .button-group form, .button-group details {
1418
- width: 100%; /* Make forms/details take full width */
1419
- }
1420
- .button-group button {
1421
- width: 100%; /* Make buttons full width */
1422
- justify-content: center; /* Center button text/icon */
1423
- }
1424
- .product-item details summary {
1425
- display: flex; /* Ensure summary behaves correctly */
1426
- }
1427
  }
1428
 
1429
  </style>
@@ -1442,7 +1248,7 @@ def admin():
1442
  <input type="text" id="add-name" name="name" required>
1443
 
1444
  <label for="add-price">Цена (с):</label>
1445
- <input type="text" id="add-price" name="price" inputmode="decimal" required placeholder="Например: 150.50">
1446
 
1447
  <label for="add-description">Описание:</label>
1448
  <textarea id="add-description" name="description" rows="4" required></textarea>
@@ -1462,7 +1268,7 @@ def admin():
1462
  <div id="add-color-inputs" class="color-input-container">
1463
  <div class="color-input-group">
1464
  <input type="text" name="colors" placeholder="Например: Красный">
1465
- <button type="button" class="remove-color-btn" onclick="removeColorInput(this)" style="display: none;"><i class="fas fa-times"></i></button>
1466
  </div>
1467
  </div>
1468
  <button type="button" class="add-color-btn" onclick="addColorInput('add-color-inputs')"><i class="fas fa-plus"></i> Добавить цвет</button>
@@ -1511,9 +1317,8 @@ def admin():
1511
  <h3>{{ product['name'] }}</h3>
1512
  <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
1513
  <p><strong>Цена:</strong> {{ product['price'] }} с</p>
1514
- <p><strong>Описание:</strong> {{ product['description'] | replace('\n', '<br>') | safe }}</p>
1515
  <p><strong>Цвета:</strong> {{ product.get('colors', [])|join(', ') if product.get('colors') else 'Нет цветов' }}</p>
1516
- <p><strong>Добавлено:</strong> {{ product.get('added_at', 'Неизвестно')[:10] }}</p> {# Display only date #}
1517
  {% if product.get('photos') and product['photos']|length > 0 %}
1518
  <div class="product-photos">
1519
  {% for photo in product['photos'] %}
@@ -1532,7 +1337,7 @@ def admin():
1532
  <label for="edit-name-{{ loop.index0 }}">Название:</label>
1533
  <input type="text" id="edit-name-{{ loop.index0 }}" name="name" value="{{ product['name'] }}" required>
1534
  <label for="edit-price-{{ loop.index0 }}">Цена (с):</label>
1535
- <input type="text" id="edit-price-{{ loop.index0 }}" name="price" inputmode="decimal" value="{{ product['price'] }}" required>
1536
  <label for="edit-description-{{ loop.index0 }}">Описание:</label>
1537
  <textarea id="edit-description-{{ loop.index0 }}" name="description" rows="4" required>{{ product['description'] }}</textarea>
1538
  <label for="edit-category-{{ loop.index0 }}">Категория:</label>
@@ -1551,13 +1356,13 @@ def admin():
1551
  {% for color in colors %}
1552
  <div class="color-input-group">
1553
  <input type="text" name="colors" value="{{ color }}">
1554
- <button type="button" class="remove-color-btn" onclick="removeColorInput(this)"><i class="fas fa-times"></i></button>
1555
  </div>
1556
  {% endfor %}
1557
  {% else %}
1558
  <div class="color-input-group">
1559
  <input type="text" name="colors" placeholder="Например: Синий">
1560
- <button type="button" class="remove-color-btn" onclick="removeColorInput(this)" style="display: none;"><i class="fas fa-times"></i></button>
1561
  </div>
1562
  {% endif %}
1563
  </div>
@@ -1584,52 +1389,41 @@ def admin():
1584
  newInputGroup.className = 'color-input-group';
1585
  newInputGroup.innerHTML = `
1586
  <input type="text" name="colors" placeholder="Еще один цвет">
1587
- <button type="button" class="remove-color-btn" onclick="removeColorInput(this)"><i class="fas fa-times"></i></button>
1588
  `;
1589
- container.querySelectorAll('.remove-color-btn').forEach(btn => btn.style.display = 'inline-flex');
 
1590
  container.appendChild(newInputGroup);
1591
  }
1592
 
1593
  function removeColorInput(button) {
1594
  const container = button.closest('.color-input-container');
1595
  button.closest('.color-input-group').remove();
 
1596
  const remainingGroups = container.querySelectorAll('.color-input-group');
1597
  if (remainingGroups.length === 1) {
1598
  const lastRemoveBtn = remainingGroups[0].querySelector('.remove-color-btn');
1599
  if(lastRemoveBtn) lastRemoveBtn.style.display = 'none';
1600
  }
 
1601
  if (remainingGroups.length === 0) {
1602
- const newInputGroup = document.createElement('div');
1603
- newInputGroup.className = 'color-input-group';
1604
- newInputGroup.innerHTML = \`
1605
- <input type="text" name="colors" placeholder="Например: Цвет">
1606
- <button type="button" class="remove-color-btn" onclick="removeColorInput(this)" style="display: none;"><i class="fas fa-times"></i></button>
1607
- \`;
1608
- container.appendChild(newInputGroup);
1609
  }
1610
  }
1611
 
1612
- document.addEventListener('DOMContentLoaded', () => {
1613
- document.querySelectorAll('.color-input-container').forEach(container => {
1614
- const groups = container.querySelectorAll('.color-input-group');
1615
- if (groups.length === 1) {
1616
- const removeBtn = groups[0].querySelector('.remove-color-btn');
1617
- if (removeBtn) removeBtn.style.display = 'none';
1618
- } else if (groups.length > 1) {
1619
- groups.forEach(group => {
1620
- const removeBtn = group.querySelector('.remove-color-btn');
1621
- if (removeBtn) removeBtn.style.display = 'inline-flex';
1622
- })
1623
- }
1624
- });
1625
  });
1626
  </script>
1627
  </body>
1628
  </html>
1629
  '''
1630
- # Sort products by added_at date descending for display in admin panel
1631
- products_sorted = sorted(products, key=lambda x: x.get('added_at', ''), reverse=True)
1632
- return render_template_string(admin_html, products=products_sorted, categories=categories, repo_id=REPO_ID)
1633
 
1634
  @app.route('/backup', methods=['POST'])
1635
  def backup():
@@ -1645,6 +1439,8 @@ def backup():
1645
  def download():
1646
  try:
1647
  download_db_from_hf()
 
 
1648
  return "Актуальная база данных успешно скачана из Hugging Face.", 200
1649
  except RepositoryNotFoundError:
1650
  return "Ошибка: Репозиторий Hugging Face не найден.", 404
@@ -1654,22 +1450,22 @@ def download():
1654
 
1655
 
1656
  if __name__ == '__main__':
 
1657
  os.makedirs('uploads', exist_ok=True)
1658
 
1659
- if HF_TOKEN_WRITE or HF_TOKEN_READ:
1660
- backup_thread = threading.Thread(target=periodic_backup, daemon=True)
1661
- backup_thread.start()
1662
- else:
1663
- logging.warning("Токены Hugging Face не установлены. Периодическое резервное копирование отключено.")
1664
 
1665
 
1666
  try:
1667
  load_data()
1668
  except Exception as e:
1669
- logging.warning(f"Не удалось первоначально загрузить/скачать базу данных: {e}. Приложение запустится с пустыми данными или существующим локальным файлом, если он есть.")
1670
 
1671
- port = int(os.environ.get("PORT", 7860))
1672
- logging.info(f"Запуск приложения на порту {port}")
1673
- app.run(host='0.0.0.0', port=port)
 
 
1674
 
1675
  # --- END OF FILE app.py ---
 
1
  # --- START OF FILE app.py ---
2
+
3
  from flask import Flask, render_template_string, request, redirect, url_for
4
  import json
5
  import os
 
20
 
21
  LOGO_URL = "https://huggingface.co/spaces/Teenagerkg/optom/resolve/main/PXL_20250419_062456154~2.jpg"
22
 
23
+ logging.basicConfig(level=logging.DEBUG)
24
 
25
  def load_data():
26
  try:
 
55
  raise
56
 
57
  def upload_db_to_hf():
 
 
 
58
  try:
59
  api = HfApi()
60
  api.upload_file(
 
70
  logging.error(f"Ошибка при загрузке резервной копии: {e}")
71
 
72
  def download_db_from_hf():
 
 
 
 
 
 
 
 
73
  try:
74
  hf_hub_download(
75
  repo_id=REPO_ID,
 
89
 
90
  def periodic_backup():
91
  while True:
 
 
92
  upload_db_to_hf()
93
+ time.sleep(800)
94
 
95
  @app.route('/')
96
  def catalog():
97
  data = load_data()
98
+ products = sorted(data['products'], key=lambda x: x.get('added_at', ''), reverse=True)
99
+ categories = data['categories']
100
 
101
  catalog_html = '''
102
  <!DOCTYPE html>
 
207
  }
208
  .products-grid {
209
  display: grid;
210
+ grid-template-columns: repeat(2, minmax(200px, 1fr));
211
  gap: 15px;
212
  padding: 10px;
 
 
 
 
 
 
 
 
 
 
213
  }
214
  .product {
215
  background: var(--light-text);
 
218
  box-shadow: 0 4px 15px var(--shadow-color);
219
  transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.3s ease;
220
  overflow: hidden;
 
 
221
  }
222
  .product:hover {
223
  transform: translateY(-5px) scale(1.02);
 
225
  }
226
  .product-image {
227
  width: 100%;
228
+ aspect-ratio: 1;
229
  background-color: #fff;
230
  border-radius: 10px;
231
  overflow: hidden;
232
  display: flex;
233
  justify-content: center;
234
  align-items: center;
 
235
  }
236
  .product-image img {
237
  max-width: 100%;
 
245
  .product h2 {
246
  font-size: 1rem;
247
  font-weight: 600;
248
+ margin: 10px 0;
249
  text-align: center;
250
  white-space: nowrap;
251
  overflow: hidden;
252
  text-overflow: ellipsis;
 
253
  }
254
  .product-price {
255
  font-size: 1.1rem;
 
257
  font-weight: 700;
258
  text-align: center;
259
  margin: 5px 0;
 
260
  }
261
  .product-description {
262
  font-size: 0.8rem;
 
266
  overflow: hidden;
267
  text-overflow: ellipsis;
268
  white-space: nowrap;
 
 
 
 
 
 
 
 
269
  }
270
  .product-button {
271
  display: block;
 
279
  font-weight: 500;
280
  cursor: pointer;
281
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
282
+ margin: 5px 0;
283
  text-align: center;
284
  text-decoration: none;
285
  }
 
308
  font-size: 1.2rem;
309
  cursor: pointer;
310
  display: none;
 
 
311
  box-shadow: 0 4px 15px rgba(255, 167, 38, 0.4);
312
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
313
  z-index: 1000;
 
325
  height: 100%;
326
  background-color: rgba(0,0,0,0.5);
327
  backdrop-filter: blur(5px);
 
328
  }
329
  .modal-content {
330
  background: var(--light-text);
 
335
  max-width: 700px;
336
  box-shadow: 0 10px 30px rgba(0,0,0,0.2);
337
  animation: slideIn 0.3s ease-out;
338
+ max-height: 85vh;
339
+ overflow-y: auto;
340
+ display: flex;
341
+ flex-direction: column;
342
+ }
343
+ #cartContent {
344
+ max-height: 60vh;
345
+ overflow-y: auto;
346
+ margin-bottom: 15px;
347
+ padding-right: 10px;
348
+ flex-grow: 1;
349
  }
350
  @keyframes slideIn {
351
  from { transform: translateY(-50px); opacity: 0; }
352
  to { transform: translateY(0); opacity: 1; }
353
  }
354
  .close {
355
+ float: right;
356
+ font-size: 1.5rem;
357
+ color: #718096;
 
 
358
  cursor: pointer;
359
  transition: color 0.3s;
360
+ align-self: flex-end;
361
  }
362
  .close:hover {
363
  color: var(--primary-color);
 
368
  align-items: center;
369
  padding: 15px 0;
370
  border-bottom: 1px solid var(--secondary-color);
 
 
 
 
 
 
 
 
371
  }
372
  .cart-item img {
373
  width: 50px;
374
  height: 50px;
375
  object-fit: contain;
376
  border-radius: 8px;
377
+ margin-right: 15px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
378
  }
379
  .quantity-input, .color-select {
380
  width: 100%;
 
399
  background-color: var(--hover-secondary-color);
400
  box-shadow: 0 4px 15px rgba(255, 160, 0, 0.4);
401
  }
402
+ .modal-footer {
403
+ margin-top: auto;
404
+ padding-top: 15px;
405
+ text-align: right;
406
+ border-top: 1px solid var(--secondary-color);
407
+ }
408
  @media (max-width: 768px) {
409
+ .products-grid {
410
+ grid-template-columns: repeat(2, minmax(150px, 1fr));
411
+ }
412
  .header h1 {
413
  font-size: 1.2rem;
414
  }
 
439
  }
440
  .modal-content {
441
  padding: 15px;
442
+ max-height: 90vh;
 
443
  }
444
+ #cartContent {
445
+ max-height: 65vh;
 
 
446
  }
447
  }
448
  @media (max-width: 480px) {
 
466
  .category-filter {
467
  padding: 5px 10px;
468
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
469
  }
470
  </style>
471
  </head>
 
492
  data-category="{{ product.get('category', 'Без категории') }}">
493
  {% if product.get('photos') and product['photos']|length > 0 %}
494
  <div class="product-image">
495
+ <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product['photos'][0] }}"
496
  alt="{{ product['name'] }}"
497
  loading="lazy">
498
  </div>
 
 
 
 
499
  {% endif %}
500
  <h2>{{ product['name'] }}</h2>
501
  <div class="product-price">{{ product['price'] }} с</div>
502
+ <p class="product-description">{{ product['description'][:50] }}{% if product['description']|length > 50 %}...{% endif %}</p>
503
+ <button class="product-button" onclick="openModal({{ loop.index0 }})">Подробнее</button>
504
+ <button class="product-button add-to-cart" onclick="openQuantityModal({{ loop.index0 }})">В корзину</button>
 
 
505
  </div>
506
  {% endfor %}
507
  </div>
 
518
  <div class="modal-content">
519
  <span class="close" onclick="closeModal('quantityModal')">×</span>
520
  <h2>Укажите количество и цвет</h2>
521
+ <input type="number" id="quantityInput" class="quantity-input" min="1" value="1">
522
+ <select id="colorSelect" class="color-select"></select>
523
  <button class="product-button" onclick="confirmAddToCart()">Добавить</button>
524
  </div>
525
  </div>
 
529
  <span class="close" onclick="closeModal('cartModal')">×</span>
530
  <h2>Корзина</h2>
531
  <div id="cartContent"></div>
532
+ <div class="modal-footer">
533
  <strong>Итого: <span id="cartTotal">0</span> с</strong>
534
  <button class="product-button clear-cart" onclick="clearCart()">Очистить</button>
535
  <button class="product-button order-button" onclick="orderViaWhatsApp()">Заказать</button>
 
539
 
540
  <button id="cart-button" onclick="openCartModal()"><i class="fas fa-shopping-cart"></i></button>
541
 
542
+ <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
543
+ <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.5.3/dist/umd/popper.min.js"></script>
544
  <script src="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.js"></script>
545
  <script>
546
  const products = {{ products|tojson }};
 
549
  function openModal(index) {
550
  loadProductDetails(index);
551
  document.getElementById('productModal').style.display = "block";
 
552
  }
553
 
554
  function closeModal(modalId) {
555
  document.getElementById(modalId).style.display = "none";
 
556
  }
557
 
558
  function loadProductDetails(index) {
559
  fetch('/product/' + index)
560
+ .then(response => response.text())
 
 
 
561
  .then(data => {
562
  document.getElementById('modalContent').innerHTML = data;
563
  initializeSwiper();
564
  })
565
+ .catch(error => console.error('Ошибка:', error));
 
 
 
566
  }
567
 
568
  function initializeSwiper() {
569
+ new Swiper('.swiper-container', {
570
+ slidesPerView: 1,
571
+ spaceBetween: 20,
572
+ loop: true,
573
+ grabCursor: true,
574
+ pagination: { el: '.swiper-pagination', clickable: true },
575
+ navigation: { nextEl: '.swiper-button-next', prevEl: '.swiper-button-prev' },
576
+ zoom: { maxRatio: 3 }
577
+ });
 
 
 
 
 
578
  }
579
 
580
  function openQuantityModal(index) {
 
582
  const product = products[index];
583
  const colorSelect = document.getElementById('colorSelect');
584
  colorSelect.innerHTML = '';
585
+ if (product.colors && product.colors.length > 0) {
586
+ product.colors.forEach(color => {
587
+ const option = document.createElement('option');
588
+ option.value = color;
589
+ option.text = color;
590
+ colorSelect.appendChild(option);
591
+ });
592
+ } else {
593
  const option = document.createElement('option');
594
+ option.value = 'Нет цвета';
595
+ option.text = 'Нет цвета';
596
  colorSelect.appendChild(option);
597
+ }
 
598
  document.getElementById('quantityModal').style.display = 'block';
599
  document.getElementById('quantityInput').value = 1;
 
600
  }
601
 
602
  function confirmAddToCart() {
 
610
  let cart = JSON.parse(localStorage.getItem('cart') || '[]');
611
  const product = products[selectedProductIndex];
612
  const cartItemId = `${product.name}-${color}`;
613
+ const existingItem = cart.find(item => item.id === cartItemId);
614
 
615
+ if (existingItem) {
616
+ existingItem.quantity += quantity;
617
  } else {
 
 
618
  cart.push({
619
  id: cartItemId,
620
  name: product.name,
621
+ price: product.price,
622
+ photo: product.photos && product.photos.length > 0 ? product.photos[0] : '',
623
  quantity: quantity,
624
  color: color
625
  });
 
634
  const cart = JSON.parse(localStorage.getItem('cart') || '[]');
635
  const cartButton = document.getElementById('cart-button');
636
  cartButton.style.display = cart.length > 0 ? 'flex' : 'none';
637
+ cartButton.style.alignItems = 'center';
638
+ cartButton.style.justifyContent = 'center';
639
  }
640
 
641
  function openCartModal() {
 
644
  let total = 0;
645
 
646
  cartContent.innerHTML = cart.length === 0 ? '<p>Корзина пуста</p>' : cart.map(item => {
647
+ const itemTotal = item.price * item.quantity;
 
 
648
  total += itemTotal;
 
 
649
  return `
650
  <div class="cart-item">
651
+ <div style="display: flex; align-items: center;">
652
+ ${item.photo ? `<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/${item.photo}" alt="${item.name}">` : ''}
653
+ <div>
654
  <strong>${item.name}</strong>
655
+ <p>${item.price} с × ${item.quantity} (Цвет: ${item.color})</p>
656
  </div>
657
  </div>
658
+ <span>${itemTotal} с</span>
659
  </div>
660
  `;
661
  }).join('');
662
 
663
+ document.getElementById('cartTotal').textContent = total;
664
  document.getElementById('cartModal').style.display = 'block';
 
665
  }
666
 
667
  function orderViaWhatsApp() {
 
673
  let total = 0;
674
  let orderText = "Заказ:%0A";
675
  cart.forEach((item, index) => {
676
+ const itemTotal = item.price * item.quantity;
 
 
677
  total += itemTotal;
678
+ orderText += `${index + 1}. ${item.name} - ${item.price} с × ${item.quantity} (Цвет: ${item.color})%0A`;
679
  });
680
+ orderText += `Итого: ${total} с`;
681
+ window.open(`https://api.whatsapp.com/send?phone=996704667878&text=${orderText}`, '_blank');
 
682
  }
683
 
684
  function clearCart() {
 
689
 
690
  window.onclick = function(event) {
691
  if (event.target.classList.contains('modal')) {
692
+ event.target.style.display = "none";
693
  }
694
  }
695
 
 
703
  });
704
 
705
  function filterProducts() {
706
+ const searchTerm = document.getElementById('search-input').value.toLowerCase();
707
  const activeCategory = document.querySelector('.category-filter.active').dataset.category;
708
+ document.querySelectorAll('.product').forEach(product => {
709
+ const name = product.getAttribute('data-name');
710
+ const description = product.getAttribute('data-description');
711
+ const category = product.getAttribute('data-category');
712
+ const matchesSearch = name.includes(searchTerm) || description.includes(searchTerm);
 
 
 
 
713
  const matchesCategory = activeCategory === 'all' || category === activeCategory;
714
+ product.style.display = matchesSearch && matchesCategory ? 'block' : 'none';
 
 
 
 
 
 
 
715
  });
716
  }
717
 
718
+ updateCartButton();
 
 
 
719
  </script>
720
  </body>
721
  </html>
 
725
  @app.route('/product/<int:index>')
726
  def product_detail(index):
727
  data = load_data()
728
+ products = data['products']
729
+ try:
730
+ product = products[index]
731
+ except IndexError:
732
  return "Продукт не найден", 404
 
733
 
734
  detail_html = '''
735
+ <div class="container" style="padding: 20px; font-family: 'Poppins', sans-serif; color: #333;">
736
+ <h2 style="font-size: 1.8rem; font-weight: 600; margin-bottom: 20px; color: #FFA726;">{{ product['name'] }}</h2>
737
+ <div class="swiper-container" style="max-width: 400px; margin: 0 auto 20px; --swiper-navigation-color: #FFA726; --swiper-pagination-color: #FFA726;">
738
  <div class="swiper-wrapper">
739
  {% if product.get('photos') and product['photos']|length > 0 %}
740
  {% for photo in product['photos'] %}
741
+ <div class="swiper-slide" style="background-color: #fff; display: flex; justify-content: center; align-items: center; border-radius: 10px;">
742
  <div class="swiper-zoom-container">
743
+ <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo }}"
744
  alt="{{ product['name'] }}"
745
+ style="max-width: 100%; max-height: 350px; object-fit: contain;">
746
  </div>
747
  </div>
748
  {% endfor %}
749
  {% else %}
750
+ <div class="swiper-slide" style="background-color: #eee; display: flex; justify-content: center; align-items: center; border-radius: 10px; height: 350px;">
751
  <span style="color: #999;">Нет изображения</span>
752
  </div>
753
  {% endif %}
754
  </div>
755
+ <div class="swiper-pagination"></div>
756
+ <div class="swiper-button-next"></div>
757
+ <div class="swiper-button-prev"></div>
 
 
758
  </div>
759
  <div style="background: #FFF8E1; padding: 15px; border-radius: 10px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
760
  <p style="margin-bottom: 10px;"><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
761
  <p style="margin-bottom: 10px;"><strong>Цена:</strong> <span style="color: #D84315; font-weight: bold;">{{ product['price'] }} с</span></p>
762
+ <p style="margin-bottom: 10px;"><strong>Описание:</strong> {{ product['description'] }}</p>
763
  <p><strong>Доступные цвета:</strong> {{ product.get('colors', ['Нет цветов'])|join(', ') if product.get('colors') else 'Нет цветов' }}</p>
764
  </div>
765
  </div>
 
770
  @app.route('/admin', methods=['GET', 'POST'])
771
  def admin():
772
  data = load_data()
773
+ products = data['products']
774
+ categories = data['categories']
775
 
776
  if request.method == 'POST':
777
  action = request.form.get('action')
778
 
779
  if action == 'add_category':
780
+ category_name = request.form.get('category_name')
781
  if category_name and category_name not in categories:
782
  categories.append(category_name)
783
+ save_data(data)
 
784
  return redirect(url_for('admin'))
785
+ return "Ошибка: Категория уже существует или не указано название", 400
 
 
 
 
786
 
787
  elif action == 'delete_category':
788
+ category_index = int(request.form.get('category_index'))
789
+ deleted_category = categories.pop(category_index)
790
+ for product in products:
791
+ if product.get('category') == deleted_category:
792
+ product['category'] = 'Без категории'
793
+ save_data(data)
794
+ return redirect(url_for('admin'))
 
 
 
 
 
 
 
 
 
795
 
796
  elif action == 'add':
797
+ name = request.form.get('name')
798
+ price = request.form.get('price')
799
+ description = request.form.get('description')
800
  category = request.form.get('category')
801
  photos_files = request.files.getlist('photos')
802
  colors = [c.strip() for c in request.form.getlist('colors') if c.strip()]
803
  photos_list = []
804
 
805
+ if photos_files:
 
 
 
 
 
 
 
 
 
 
 
 
806
  for photo in photos_files[:10]:
807
  if photo and photo.filename:
808
  photo_filename = secure_filename(f"{int(time.time())}_{photo.filename}")
809
+ uploads_dir = 'uploads'
810
+ os.makedirs(uploads_dir, exist_ok=True)
811
  temp_path = os.path.join(uploads_dir, photo_filename)
812
  try:
813
  photo.save(temp_path)
 
821
  commit_message=f"Добавлено фото для товара {name}"
822
  )
823
  photos_list.append(photo_filename)
 
824
  except Exception as e:
825
  logging.error(f"Ошибка при загрузке фото {photo_filename}: {e}")
826
  finally:
 
829
  os.remove(temp_path)
830
  except OSError as e:
831
  logging.error(f"Ошибка при удалении временного файла {temp_path}: {e}")
832
+
833
+
834
+ if not name or not price or not description:
835
+ return "Ошибка: Заполните все обязательные поля", 400
836
+
837
+ try:
838
+ price = float(price.replace(',', '.'))
839
+ except ValueError:
840
+ return "Ошибка: Неверный формат цены", 400
841
 
842
  new_product = {
843
  'name': name,
 
849
  'added_at': datetime.now().isoformat()
850
  }
851
  products.append(new_product)
852
+ save_data(data)
 
853
  return redirect(url_for('admin'))
854
 
855
  elif action == 'edit':
 
856
  try:
857
+ index = int(request.form.get('index'))
858
  if not 0 <= index < len(products):
859
+ return "Ошибка: Неверный индекс товара", 400
860
  except (ValueError, TypeError):
861
+ return "Ошибка: Неверный индекс товара", 400
862
 
863
+ name = request.form.get('name')
864
+ price = request.form.get('price')
865
+ description = request.form.get('description')
866
  category = request.form.get('category')
867
  photos_files = request.files.getlist('photos')
868
  colors = [c.strip() for c in request.form.getlist('colors') if c.strip()]
869
 
870
+ if not name or not price or not description:
871
+ return "Ошибка: Заполните все обязательные поля", 400
872
 
873
  try:
874
+ price_float = float(price.replace(',', '.'))
 
875
  except ValueError:
876
  return "Ошибка: Неверный формат цены", 400
877
 
 
 
 
878
 
879
+ if photos_files and any(photo.filename for photo in photos_files):
880
+ new_photos_list = []
881
  for photo in photos_files[:10]:
882
  if photo and photo.filename:
883
  photo_filename = secure_filename(f"{int(time.time())}_{photo.filename}")
884
+ uploads_dir = 'uploads'
885
+ os.makedirs(uploads_dir, exist_ok=True)
886
  temp_path = os.path.join(uploads_dir, photo_filename)
887
  try:
888
  photo.save(temp_path)
 
896
  commit_message=f"Обновлено фото для товара {name}"
897
  )
898
  new_photos_list.append(photo_filename)
 
899
  except Exception as e:
900
  logging.error(f"Ошибка при загрузке фото {photo_filename} при редактировании: {e}")
901
  finally:
 
904
  os.remove(temp_path)
905
  except OSError as e:
906
  logging.error(f"Ошибка при удалении временного файла {temp_path} при редактировании: {e}")
 
 
 
 
907
 
908
 
909
+ products[index]['photos'] = new_photos_list
910
+
911
  products[index]['name'] = name
912
  products[index]['price'] = price_float
913
  products[index]['description'] = description
914
  products[index]['category'] = category if category in categories else 'Без категории'
915
  products[index]['colors'] = colors
916
 
917
+ save_data(data)
 
918
  return redirect(url_for('admin'))
919
 
920
  elif action == 'delete':
 
921
  try:
922
+ index = int(request.form.get('index'))
923
  if not 0 <= index < len(products):
924
+ return "Ошибка: Неверный индекс товара", 400
925
 
 
 
926
  del products[index]
927
+ save_data(data)
 
928
  except (ValueError, TypeError):
929
+ return "Ошибка: Неверный индекс товара", 400
930
  except Exception as e:
931
  logging.error(f"Ошибка при удалении товара: {e}")
932
  return "Ошибка при удалении товара", 500
 
1037
  padding: 10px;
1038
  background-color: #fff;
1039
  }
1040
+
1041
+ input[type="file"]::file-selector-button {
1042
  padding: 8px 15px;
1043
  border: none;
1044
  border-radius: 6px;
 
1066
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
1067
  margin-top: 20px;
1068
  margin-right: 10px;
 
 
 
1069
  }
1070
  button[type="submit"]:hover, button[type="button"]:hover {
1071
  background-color: var(--hover-primary-color);
 
1106
  display: flex;
1107
  justify-content: space-between;
1108
  align-items: center;
 
 
1109
  }
1110
  .category-item h3 {
1111
  margin: 0;
1112
  color: var(--text-color);
1113
  font-weight: 500;
 
1114
  }
1115
  .category-item form {
1116
  background: none;
 
1131
  margin-bottom: 8px;
1132
  font-size: 0.9rem;
1133
  line-height: 1.5;
 
1134
  }
1135
  .product-item p strong {
1136
  color: #555;
 
1158
  color: var(--primary-color);
1159
  padding: 8px 0;
1160
  outline: none;
1161
+ display: inline-block;
 
 
1162
  }
 
 
 
 
 
 
 
 
 
1163
  .product-item summary:hover {
1164
  color: var(--hover-primary-color);
1165
  }
 
1185
  flex-grow: 1;
1186
  }
1187
  .color-input-group .remove-color-btn {
1188
+ padding: 5px 10px;
1189
  font-size: 0.8rem;
1190
  background-color: #ddd;
1191
  color: #333;
1192
  border-radius: 50%;
1193
  margin-top: 0;
1194
+ min-width: 25px;
1195
  height: 25px;
1196
+ line-height: 1;
 
 
 
 
 
1197
  }
1198
  .color-input-group .remove-color-btn:hover {
1199
  background-color: #ccc;
 
1204
  .button-group {
1205
  margin-top: 20px;
1206
  display: flex;
 
1207
  gap: 10px;
1208
  }
1209
  .button-group form {
 
1211
  padding: 0;
1212
  margin: 0;
1213
  box-shadow: none;
 
1214
  }
1215
+ .button-group button {
1216
  margin-top: 0;
1217
  }
1218
 
 
1230
  form {
1231
  padding: 20px;
1232
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1233
  }
1234
 
1235
  </style>
 
1248
  <input type="text" id="add-name" name="name" required>
1249
 
1250
  <label for="add-price">Цена (с):</label>
1251
+ <input type="number" id="add-price" name="price" step="0.01" required placeholder="Например: 150.50">
1252
 
1253
  <label for="add-description">Описание:</label>
1254
  <textarea id="add-description" name="description" rows="4" required></textarea>
 
1268
  <div id="add-color-inputs" class="color-input-container">
1269
  <div class="color-input-group">
1270
  <input type="text" name="colors" placeholder="Например: Красный">
1271
+ <button type="button" class="remove-color-btn" onclick="removeColorInput(this)" style="display: none;">X</button>
1272
  </div>
1273
  </div>
1274
  <button type="button" class="add-color-btn" onclick="addColorInput('add-color-inputs')"><i class="fas fa-plus"></i> Добавить цвет</button>
 
1317
  <h3>{{ product['name'] }}</h3>
1318
  <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
1319
  <p><strong>Цена:</strong> {{ product['price'] }} с</p>
1320
+ <p><strong>Описание:</strong> {{ product['description'] }}</p>
1321
  <p><strong>Цвета:</strong> {{ product.get('colors', [])|join(', ') if product.get('colors') else 'Нет цветов' }}</p>
 
1322
  {% if product.get('photos') and product['photos']|length > 0 %}
1323
  <div class="product-photos">
1324
  {% for photo in product['photos'] %}
 
1337
  <label for="edit-name-{{ loop.index0 }}">Название:</label>
1338
  <input type="text" id="edit-name-{{ loop.index0 }}" name="name" value="{{ product['name'] }}" required>
1339
  <label for="edit-price-{{ loop.index0 }}">Цена (с):</label>
1340
+ <input type="number" id="edit-price-{{ loop.index0 }}" name="price" step="0.01" value="{{ product['price'] }}" required>
1341
  <label for="edit-description-{{ loop.index0 }}">Описание:</label>
1342
  <textarea id="edit-description-{{ loop.index0 }}" name="description" rows="4" required>{{ product['description'] }}</textarea>
1343
  <label for="edit-category-{{ loop.index0 }}">Категория:</label>
 
1356
  {% for color in colors %}
1357
  <div class="color-input-group">
1358
  <input type="text" name="colors" value="{{ color }}">
1359
+ <button type="button" class="remove-color-btn" onclick="removeColorInput(this)">X</button>
1360
  </div>
1361
  {% endfor %}
1362
  {% else %}
1363
  <div class="color-input-group">
1364
  <input type="text" name="colors" placeholder="Например: Синий">
1365
+ <button type="button" class="remove-color-btn" onclick="removeColorInput(this)" style="display: none;">X</button>
1366
  </div>
1367
  {% endif %}
1368
  </div>
 
1389
  newInputGroup.className = 'color-input-group';
1390
  newInputGroup.innerHTML = `
1391
  <input type="text" name="colors" placeholder="Еще один цвет">
1392
+ <button type="button" class="remove-color-btn" onclick="removeColorInput(this)">X</button>
1393
  `;
1394
+
1395
+ container.querySelectorAll('.remove-color-btn').forEach(btn => btn.style.display = 'inline-block');
1396
  container.appendChild(newInputGroup);
1397
  }
1398
 
1399
  function removeColorInput(button) {
1400
  const container = button.closest('.color-input-container');
1401
  button.closest('.color-input-group').remove();
1402
+
1403
  const remainingGroups = container.querySelectorAll('.color-input-group');
1404
  if (remainingGroups.length === 1) {
1405
  const lastRemoveBtn = remainingGroups[0].querySelector('.remove-color-btn');
1406
  if(lastRemoveBtn) lastRemoveBtn.style.display = 'none';
1407
  }
1408
+
1409
  if (remainingGroups.length === 0) {
1410
+ addColorInput(container.id);
 
 
 
 
 
 
1411
  }
1412
  }
1413
 
1414
+
1415
+ document.querySelectorAll('.color-input-container').forEach(container => {
1416
+ const groups = container.querySelectorAll('.color-input-group');
1417
+ if (groups.length === 1) {
1418
+ const removeBtn = groups[0].querySelector('.remove-color-btn');
1419
+ if (removeBtn) removeBtn.style.display = 'none';
1420
+ }
 
 
 
 
 
 
1421
  });
1422
  </script>
1423
  </body>
1424
  </html>
1425
  '''
1426
+ return render_template_string(admin_html, products=products, categories=categories, repo_id=REPO_ID)
 
 
1427
 
1428
  @app.route('/backup', methods=['POST'])
1429
  def backup():
 
1439
  def download():
1440
  try:
1441
  download_db_from_hf()
1442
+
1443
+
1444
  return "Актуальная база данных успешно скачана из Hugging Face.", 200
1445
  except RepositoryNotFoundError:
1446
  return "Ошибка: Репозиторий Hugging Face не найден.", 404
 
1450
 
1451
 
1452
  if __name__ == '__main__':
1453
+
1454
  os.makedirs('uploads', exist_ok=True)
1455
 
1456
+
1457
+ backup_thread = threading.Thread(target=periodic_backup, daemon=True)
1458
+ backup_thread.start()
 
 
1459
 
1460
 
1461
  try:
1462
  load_data()
1463
  except Exception as e:
 
1464
 
1465
+ logging.warning(f"Не удалось первоначально загрузить/скачать базу данных: {e}. Приложение запустится с пустыми данными.")
1466
+
1467
+
1468
+
1469
+ app.run(debug=False, host='0.0.0.0', port=int(os.environ.get("PORT", 7860)))
1470
 
1471
  # --- END OF FILE app.py ---