Kgshop commited on
Commit
373b35d
·
verified ·
1 Parent(s): 45cc88c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +731 -342
app.py CHANGED
@@ -25,18 +25,14 @@ def load_data():
25
  download_db_from_hf()
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
32
  except FileNotFoundError:
33
- logging.warning("Локальный файл базы данных не найден после скачивания.")
34
  return {'products': [], 'categories': []}
35
  except json.JSONDecodeError:
36
- logging.error("Ошибка: Невозможно декодировать JSON файл.")
37
  return {'products': [], 'categories': []}
38
  except RepositoryNotFoundError:
39
- logging.error("Репозиторий не найден. Создание локальной базы данных.")
40
  return {'products': [], 'categories': []}
41
  except Exception as e:
42
  logging.error(f"Произошла ошибка при загрузке данных: {e}")
@@ -46,7 +42,6 @@ def save_data(data):
46
  try:
47
  with open(DATA_FILE, 'w', encoding='utf-8') as file:
48
  json.dump(data, file, ensure_ascii=False, indent=4)
49
- logging.info("Данные успешно сохранены в JSON")
50
  upload_db_to_hf()
51
  except Exception as e:
52
  logging.error(f"Ошибка при сохранении данных: {e}")
@@ -63,7 +58,6 @@ def upload_db_to_hf():
63
  token=HF_TOKEN_WRITE,
64
  commit_message=f"Автоматическое резервное копирование базы данных {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
65
  )
66
- logging.info("Резервная копия JSON базы успешно загружена на Hugging Face.")
67
  except Exception as e:
68
  logging.error(f"Ошибка при загрузке резервной копии: {e}")
69
 
@@ -77,7 +71,6 @@ def download_db_from_hf():
77
  local_dir=".",
78
  local_dir_use_symlinks=False
79
  )
80
- logging.info("JSON база успешно скачана из Hugging Face.")
81
  except RepositoryNotFoundError as e:
82
  logging.error(f"Репозиторий не найден: {e}")
83
  raise
@@ -95,36 +88,72 @@ def catalog():
95
  data = load_data()
96
  products = data['products']
97
  categories = data['categories']
98
-
99
  catalog_html = '''
100
  <!DOCTYPE html>
101
  <html lang="ru">
102
  <head>
103
  <meta charset="UTF-8">
104
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
105
- <title>Zalkar Textile - ткани на заказ </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
  * {
111
  margin: 0;
112
  padding: 0;
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
  }
@@ -133,7 +162,12 @@ def catalog():
133
  justify-content: space-between;
134
  align-items: center;
135
  padding: 15px 0;
136
- border-bottom: 1px solid #e2e8f0;
 
 
 
 
 
137
  }
138
  .header-logo {
139
  width: 60px;
@@ -148,20 +182,25 @@ def catalog():
148
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
149
  }
150
  .header h1 {
151
- font-size: 1.5rem;
152
- font-weight: 600;
 
153
  margin-left: 15px;
 
154
  }
 
 
 
155
  .theme-toggle {
156
  background: none;
157
  border: none;
158
  font-size: 1.5rem;
159
  cursor: pointer;
160
- color: #4a5568;
161
  transition: color 0.3s ease;
162
  }
163
  .theme-toggle:hover {
164
- color: #3b82f6;
165
  }
166
  .filters-container {
167
  margin: 20px 0;
@@ -176,149 +215,184 @@ def catalog():
176
  }
177
  #search-input {
178
  width: 90%;
179
- max-width: 600px;
180
- padding: 12px 18px;
181
- font-size: 1rem;
182
- border: 1px solid #e2e8f0;
183
- border-radius: 8px;
184
  outline: none;
185
- box-shadow: 0 2px 5px rgba(0,0,0,0.05);
186
  transition: all 0.3s ease;
 
 
187
  }
188
  #search-input:focus {
189
- border-color: #3b82f6;
190
- box-shadow: 0 4px 15px rgba(59, 130, 246, 0.2);
191
  }
192
  .category-filter {
193
- padding: 8px 16px;
194
- border: 1px solid #e2e8f0;
195
- border-radius: 8px;
196
- background-color: #fff;
197
  cursor: pointer;
198
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
199
  font-size: 0.9rem;
200
  font-weight: 400;
 
201
  }
202
  .category-filter.active, .category-filter:hover {
203
- background-color: #3b82f6;
204
- color: white;
205
- border-color: #3b82f6;
206
- box-shadow: 0 2px 10px rgba(59, 130, 246, 0.3);
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: #fff;
216
  border-radius: 15px;
217
- padding: 15px;
218
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
219
- transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.3s ease;
220
  overflow: hidden;
221
- }
222
- body.dark-mode .product {
223
- background: #2d3748;
224
- color: #fff;
225
  }
226
  .product:hover {
227
- transform: translateY(-5px) scale(1.02);
228
- box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
229
  }
230
  .product-image {
231
  width: 100%;
232
  aspect-ratio: 1;
233
- background-color: #fff;
234
  border-radius: 10px;
235
  overflow: hidden;
236
  display: flex;
237
  justify-content: center;
238
  align-items: center;
 
 
 
 
 
239
  }
240
  .product-image img {
241
- max-width: 100%;
242
- max-height: 100%;
243
- object-fit: contain;
244
- transition: transform 0.3s ease;
245
  }
246
  .product-image img:hover {
247
  transform: scale(1.1);
248
  }
249
  .product h2 {
250
- font-size: 1rem;
251
- font-weight: 600;
252
- margin: 10px 0;
 
253
  text-align: center;
 
254
  white-space: nowrap;
255
  overflow: hidden;
256
  text-overflow: ellipsis;
257
  }
 
 
 
258
  .product-price {
259
- font-size: 1.1rem;
260
- color: #ef4444;
261
  font-weight: 700;
262
  text-align: center;
263
  margin: 5px 0;
264
  }
265
  .product-description {
266
- font-size: 0.8rem;
267
- color: #718096;
268
  text-align: center;
269
  margin-bottom: 15px;
270
  overflow: hidden;
271
  text-overflow: ellipsis;
272
- white-space: nowrap;
 
 
 
273
  }
274
- body.dark-mode .product-description {
275
- color: #a0aec0;
 
 
276
  }
277
  .product-button {
278
  display: block;
279
  width: 100%;
280
- padding: 8px;
281
  border: none;
282
  border-radius: 8px;
283
- background-color: #3b82f6;
284
- color: white;
285
- font-size: 0.8rem;
286
  font-weight: 500;
287
  cursor: pointer;
288
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
289
- margin: 5px 0;
290
  text-align: center;
291
  text-decoration: none;
 
292
  }
293
  .product-button:hover {
294
- background-color: #2563eb;
295
- box-shadow: 0 4px 15px rgba(37, 99, 235, 0.4);
296
  transform: translateY(-2px);
297
  }
298
  .add-to-cart {
299
- background-color: #10b981;
300
  }
301
  .add-to-cart:hover {
302
- background-color: #059669;
303
- box-shadow: 0 4px 15px rgba(5, 150, 105, 0.4);
304
  }
305
  #cart-button {
306
  position: fixed;
307
- bottom: 20px;
308
- right: 20px;
309
- background-color: #ef4444;
310
  color: white;
311
  border: none;
312
  border-radius: 50%;
313
- width: 50px;
314
- height: 50px;
315
- font-size: 1.2rem;
316
  cursor: pointer;
317
  display: none;
318
- box-shadow: 0 4px 15px rgba(239, 68, 68, 0.4);
319
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
320
  z-index: 1000;
 
 
 
321
  }
 
 
 
 
 
 
322
  .modal {
323
  display: none;
324
  position: fixed;
@@ -327,90 +401,253 @@ def catalog():
327
  top: 0;
328
  width: 100%;
329
  height: 100%;
330
- background-color: rgba(0,0,0,0.5);
331
- backdrop-filter: blur(5px);
 
 
332
  }
333
  .modal-content {
334
- background: #fff;
335
  margin: 5% auto;
336
- padding: 20px;
337
- border-radius: 15px;
338
- width: 90%;
339
- max-width: 700px;
340
- box-shadow: 0 10px 30px rgba(0,0,0,0.2);
341
- animation: slideIn 0.3s ease-out;
342
- }
343
- body.dark-mode .modal-content {
344
- background: #2d3748;
345
- color: #e2e8f0;
346
  }
347
  @keyframes slideIn {
348
- from { transform: translateY(-50px); opacity: 0; }
349
  to { transform: translateY(0); opacity: 1; }
350
  }
351
  .close {
352
  float: right;
353
- font-size: 1.5rem;
354
- color: #718096;
 
355
  cursor: pointer;
356
  transition: color 0.3s;
 
 
357
  }
358
  .close:hover {
359
- color: #2d3748;
360
- }
361
- body.dark-mode .close {
362
- color: #a0aec0;
363
- }
364
- body.dark-mode .close:hover {
365
- color: #fff;
366
  }
367
  .cart-item {
368
  display: flex;
369
  justify-content: space-between;
370
  align-items: center;
371
  padding: 15px 0;
372
- border-bottom: 1px solid #e2e8f0;
373
  }
374
- body.dark-mode .cart-item {
375
- border-bottom: 1px solid #4a5568;
376
  }
377
- .cart-item img {
378
- width: 50px;
379
- height: 50px;
380
- object-fit: contain;
381
- border-radius: 8px;
 
 
 
 
382
  margin-right: 15px;
 
383
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
384
  .quantity-input, .color-select {
385
  width: 100%;
386
- max-width: 150px;
387
- padding: 8px;
388
- border: 1px solid #e2e8f0;
389
  border-radius: 8px;
390
  font-size: 1rem;
391
- margin: 5px 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
392
  }
 
 
 
 
 
 
 
 
 
 
393
  .clear-cart {
394
- background-color: #ef4444;
395
  }
396
  .clear-cart:hover {
397
- background-color: #dc2626;
398
- box-shadow: 0 4px 15px rgba(220, 38, 38, 0.4);
399
  }
400
  .order-button {
401
- background-color: #10b981;
402
  }
403
  .order-button:hover {
404
- background-color: #059669;
405
- box-shadow: 0 4px 15px rgba(5, 150, 105, 0.4);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
406
  }
407
  </style>
408
  </head>
409
  <body>
410
  <div class="container">
411
  <div class="header">
412
- <img src="''' + LOGO_URL + '''" alt="Logo" class="header-logo">
413
- <h1>Каталог</h1>
 
 
414
  <button class="theme-toggle" onclick="toggleTheme()">
415
  <i class="fas fa-moon"></i>
416
  </button>
@@ -422,26 +659,32 @@ def catalog():
422
  {% endfor %}
423
  </div>
424
  <div class="search-container">
425
- <input type="text" id="search-input" placeholder="Поиск товаров...">
426
  </div>
427
  <div class="products-grid" id="products-grid">
428
  {% for product in products %}
429
- <div class="product"
430
- data-name="{{ product['name']|lower }}"
431
  data-description="{{ product['description']|lower }}"
432
  data-category="{{ product.get('category', 'Без категории') }}">
433
  {% if product.get('photos') and product['photos']|length > 0 %}
434
  <div class="product-image">
435
- <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product['photos'][0] }}"
436
- alt="{{ product['name'] }}"
437
  loading="lazy">
438
  </div>
 
 
 
 
439
  {% endif %}
440
  <h2>{{ product['name'] }}</h2>
441
- <div class="product-price">{{ product['price'] }} с</div>
442
- <p class="product-description">{{ product['description'][:50] }}{% if product['description']|length > 50 %}...{% endif %}</p>
443
- <button class="product-button" onclick="openModal({{ loop.index0 }})">Подробнее</button>
444
- <button class="product-button add-to-cart" onclick="openQuantityModal({{ loop.index0 }})">В корзину</button>
 
 
445
  </div>
446
  {% endfor %}
447
  </div>
@@ -455,12 +698,12 @@ def catalog():
455
  </div>
456
 
457
  <div id="quantityModal" class="modal">
458
- <div class="modal-content">
459
  <span class="close" onclick="closeModal('quantityModal')">×</span>
460
  <h2>Укажите количество и цвет</h2>
461
  <input type="number" id="quantityInput" class="quantity-input" step="0.1" min="0.1" value="1">
462
  <select id="colorSelect" class="color-select"></select>
463
- <button class="product-button" onclick="confirmAddToCart()">Добавить</button>
464
  </div>
465
  </div>
466
 
@@ -469,18 +712,16 @@ def catalog():
469
  <span class="close" onclick="closeModal('cartModal')">×</span>
470
  <h2>Корзина</h2>
471
  <div id="cartContent"></div>
472
- <div style="margin-top: 20px; text-align: right;">
473
- <strong>Итого: <span id="cartTotal">0</span> с</strong>
474
  <button class="product-button clear-cart" onclick="clearCart()">Очистить</button>
475
- <button class="product-button order-button" onclick="orderViaWhatsApp()">Заказать</button>
476
  </div>
477
  </div>
478
  </div>
479
 
480
  <button id="cart-button" onclick="openCartModal()">🛒</button>
481
 
482
- <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
483
- <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.5.3/dist/umd/popper.min.js"></script>
484
  <script src="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.js"></script>
485
  <script>
486
  const products = {{ products|tojson }};
@@ -519,15 +760,17 @@ def catalog():
519
  }
520
 
521
  function initializeSwiper() {
522
- new Swiper('.swiper-container', {
523
- slidesPerView: 1,
524
- spaceBetween: 20,
525
- loop: true,
526
- grabCursor: true,
527
- pagination: { el: '.swiper-pagination', clickable: true },
528
- navigation: { nextEl: '.swiper-button-next', prevEl: '.swiper-button-prev' },
529
- zoom: { maxRatio: 3 }
530
- });
 
 
531
  }
532
 
533
  function openQuantityModal(index) {
@@ -544,8 +787,8 @@ def catalog():
544
  });
545
  } else {
546
  const option = document.createElement('option');
547
- option.value = 'Нет цвета';
548
- option.text = 'Нет цвета';
549
  colorSelect.appendChild(option);
550
  }
551
  document.getElementById('quantityModal').style.display = 'block';
@@ -587,7 +830,12 @@ def catalog():
587
 
588
  function updateCartButton() {
589
  const cart = JSON.parse(localStorage.getItem('cart') || '[]');
590
- document.getElementById('cart-button').style.display = cart.length > 0 ? 'block' : 'none';
 
 
 
 
 
591
  }
592
 
593
  function openCartModal() {
@@ -595,19 +843,20 @@ def catalog():
595
  const cartContent = document.getElementById('cartContent');
596
  let total = 0;
597
 
598
- cartContent.innerHTML = cart.length === 0 ? '<p>Корзина пуста</p>' : cart.map(item => {
599
  const itemTotal = item.price * item.quantity;
600
  total += itemTotal;
 
601
  return `
602
  <div class="cart-item">
603
- <div style="display: flex; align-items: center;">
604
- ${item.photo ? `<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/${item.photo}" alt="${item.name}">` : ''}
605
  <div>
606
  <strong>${item.name}</strong>
607
- <p>${item.price} с × ${item.quantity.toFixed(2)} (Цвет: ${item.color})</p>
608
  </div>
609
  </div>
610
- <span>${itemTotal.toFixed(2)} с</span>
611
  </div>
612
  `;
613
  }).join('');
@@ -627,20 +876,22 @@ def catalog():
627
  cart.forEach((item, index) => {
628
  const itemTotal = item.price * item.quantity;
629
  total += itemTotal;
630
- orderText += `${index + 1}. ${item.name} - ${item.price} с × ${item.quantity.toFixed(2)} (Цвет: ${item.color})%0A`;
631
  });
632
- orderText += `Итого: ${total.toFixed(2)} с`;
633
  window.open(`https://api.whatsapp.com/send?phone=996707842888&text=${orderText}`, '_blank');
634
  }
635
 
636
  function clearCart() {
637
  localStorage.removeItem('cart');
638
- closeModal('cartModal');
639
  updateCartButton();
640
  }
641
 
642
  window.onclick = function(event) {
643
- if (event.target.className === 'modal') event.target.style.display = "none";
 
 
644
  }
645
 
646
  document.getElementById('search-input').addEventListener('input', filterProducts);
@@ -661,7 +912,7 @@ def catalog():
661
  const category = product.getAttribute('data-category');
662
  const matchesSearch = name.includes(searchTerm) || description.includes(searchTerm);
663
  const matchesCategory = activeCategory === 'all' || category === activeCategory;
664
- product.style.display = matchesSearch && matchesCategory ? 'block' : 'none';
665
  });
666
  }
667
 
@@ -681,23 +932,23 @@ def product_detail(index):
681
  except IndexError:
682
  return "Продукт не найден", 404
683
  detail_html = '''
684
- <div class="container" style="padding: 20px;">
685
- <h2 style="font-size: 1.8rem; font-weight: 600; margin-bottom: 20px;">{{ product['name'] }}</h2>
686
- <div class="swiper-container" style="max-width: 400px; margin: 0 auto 20px;">
687
  <div class="swiper-wrapper">
688
  {% if product.get('photos') %}
689
  {% for photo in product['photos'] %}
690
- <div class="swiper-slide" style="background-color: #fff; display: flex; justify-content: center; align-items: center;">
691
  <div class="swiper-zoom-container">
692
- <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo }}"
693
- alt="{{ product['name'] }}"
694
- style="max-width: 100%; max-height: 300px; object-fit: contain;">
695
  </div>
696
  </div>
697
  {% endfor %}
698
  {% else %}
699
  <div class="swiper-slide">
700
- <img src="https://via.placeholder.com/300" alt="No Image">
701
  </div>
702
  {% endif %}
703
  </div>
@@ -706,9 +957,9 @@ def product_detail(index):
706
  <div class="swiper-button-prev"></div>
707
  </div>
708
  <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
709
- <p><strong>Цена:</strong> {{ product['price'] }} с</p>
710
  <p><strong>Описание:</strong> {{ product['description'] }}</p>
711
- <p><strong>Доступные цвета:</strong> {{ product.get('colors', ['Нет цветов'])|join(', ') }}</p>
712
  </div>
713
  '''
714
  return render_template_string(detail_html, product=product, repo_id=REPO_ID)
@@ -721,7 +972,7 @@ def admin():
721
 
722
  if request.method == 'POST':
723
  action = request.form.get('action')
724
-
725
  if action == 'add_category':
726
  category_name = request.form.get('category_name')
727
  if category_name and category_name not in categories:
@@ -731,13 +982,19 @@ def admin():
731
  return "Ошибка: Категория уже существует или не указано название", 400
732
 
733
  elif action == 'delete_category':
734
- category_index = int(request.form.get('category_index'))
735
- deleted_category = categories.pop(category_index)
736
- for product in products:
737
- if product.get('category') == deleted_category:
738
- product['category'] = 'Без категории'
739
- save_data(data)
740
- return redirect(url_for('admin'))
 
 
 
 
 
 
741
 
742
  elif action == 'add':
743
  name = request.form.get('name')
@@ -747,90 +1004,125 @@ def admin():
747
  photos_files = request.files.getlist('photos')
748
  colors = request.form.getlist('colors')
749
  photos_list = []
750
-
751
  if photos_files:
 
 
 
752
  for photo in photos_files[:10]:
753
  if photo and photo.filename:
754
  photo_filename = secure_filename(photo.filename)
755
- uploads_dir = 'uploads'
756
- os.makedirs(uploads_dir, exist_ok=True)
757
  temp_path = os.path.join(uploads_dir, photo_filename)
758
  photo.save(temp_path)
759
- api = HfApi()
760
- api.upload_file(
761
- path_or_fileobj=temp_path,
762
- path_in_repo=f"photos/{photo_filename}",
763
- repo_id=REPO_ID,
764
- repo_type="dataset",
765
- token=HF_TOKEN_WRITE,
766
- commit_message=f"Добавлено фото для товара {name}"
767
- )
768
- photos_list.append(photo_filename)
769
- if os.path.exists(temp_path):
770
- os.remove(temp_path)
771
-
 
 
 
772
  if not name or not price or not description:
773
  return "Ошибка: Заполните все обязательные поля", 400
774
-
775
- price = float(price.replace(',', '.'))
 
 
 
 
776
  new_product = {
777
  'name': name,
778
  'price': price,
779
  'description': description,
780
  'category': category if category in categories else 'Без категории',
781
  'photos': photos_list,
782
- 'colors': [c for c in colors if c.strip()] if colors else []
783
  }
784
  products.append(new_product)
785
  save_data(data)
786
  return redirect(url_for('admin'))
787
-
788
  elif action == 'edit':
789
- index = int(request.form.get('index'))
790
- name = request.form.get('name')
791
- price = request.form.get('price')
792
- description = request.form.get('description')
793
- category = request.form.get('category')
794
- photos_files = request.files.getlist('photos')
795
- colors = request.form.getlist('colors')
796
-
797
- if photos_files and any(photo.filename for photo in photos_files):
798
- new_photos_list = []
799
- for photo in photos_files[:10]:
800
- if photo and photo.filename:
801
- photo_filename = secure_filename(photo.filename)
802
- uploads_dir = 'uploads'
803
- os.makedirs(uploads_dir, exist_ok=True)
804
- temp_path = os.path.join(uploads_dir, photo_filename)
805
- photo.save(temp_path)
806
- api = HfApi()
807
- api.upload_file(
808
- path_or_fileobj=temp_path,
809
- path_in_repo=f"photos/{photo_filename}",
810
- repo_id=REPO_ID,
811
- repo_type="dataset",
812
- token=HF_TOKEN_WRITE,
813
- commit_message=f"Обновлено фото для товара {name}"
814
- )
815
- new_photos_list.append(photo_filename)
816
- if os.path.exists(temp_path):
817
- os.remove(temp_path)
818
- products[index]['photos'] = new_photos_list
819
-
820
- products[index]['name'] = name
821
- products[index]['price'] = float(price.replace(',', '.'))
822
- products[index]['description'] = description
823
- products[index]['category'] = category if category in categories else 'Без категории'
824
- products[index]['colors'] = [c for c in colors if c.strip()] if colors else []
825
- save_data(data)
826
- return redirect(url_for('admin'))
827
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
828
  elif action == 'delete':
829
- index = int(request.form.get('index'))
830
- del products[index]
831
- save_data(data)
832
- return redirect(url_for('admin'))
833
-
 
 
 
 
 
834
  admin_html = '''
835
  <!DOCTYPE html>
836
  <html lang="ru">
@@ -838,90 +1130,102 @@ def admin():
838
  <meta charset="UTF-8">
839
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
840
  <title>Админ-панель</title>
841
- <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
842
  <style>
843
  body {
844
- font-family: 'Poppins', sans-serif;
845
  background: linear-gradient(135deg, #f0f2f5, #e9ecef);
846
- color: #2d3748;
847
  padding: 20px;
 
848
  }
849
  .container {
850
  max-width: 1200px;
851
  margin: 0 auto;
 
 
 
 
852
  }
853
  .header {
854
  display: flex;
855
  align-items: center;
856
- padding: 15px 0;
857
- border-bottom: 1px solid #e2e8f0;
 
858
  }
859
  .header-logo {
860
- width: 60px;
861
- height: 60px;
862
  border-radius: 50%;
863
  object-fit: cover;
864
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
865
- transition: transform 0.3s ease, box-shadow 0.3s ease;
866
  margin-right: 15px;
867
- }
868
- .header-logo:hover {
869
- transform: scale(1.1);
870
- box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
871
  }
872
  h1, h2 {
873
- font-weight: 600;
874
  margin-bottom: 20px;
 
875
  }
 
 
 
 
 
876
  form {
877
- background: #fff;
878
- padding: 20px;
879
- border-radius: 15px;
880
- box-shadow: 0 4px 15px rgba(0,0,0,0.1);
881
  margin-bottom: 30px;
 
 
 
 
882
  }
883
  label {
884
  font-weight: 500;
885
  margin-top: 15px;
886
  display: block;
 
887
  }
888
- input, textarea, select {
889
  width: 100%;
890
- padding: 12px;
891
- margin-top: 5px;
892
- border: 1px solid #e2e8f0;
893
- border-radius: 8px;
894
  font-size: 1rem;
895
- transition: all 0.3s ease;
896
  box-sizing: border-box;
897
  }
 
 
 
898
  input:focus, textarea:focus, select:focus {
899
- border-color: #3b82f6;
900
- box-shadow: 0 0 5px rgba(59, 130, 246, 0.3);
901
  outline: none;
902
  }
903
  button {
904
- padding: 12px 20px;
905
  border: none;
906
- border-radius: 8px;
907
- background-color: #3b82f6;
908
  color: white;
909
  font-weight: 500;
910
  cursor: pointer;
911
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
912
  margin-top: 15px;
 
 
 
 
913
  }
914
  button:hover {
915
- background-color: #2563eb;
916
- box-shadow: 0 4px 15px rgba(37, 99, 235, 0.4);
917
- transform: translateY(-2px);
918
  }
919
  .delete-button {
920
- background-color: #ef4444;
921
  }
922
  .delete-button:hover {
923
- background-color: #dc2626;
924
- box-shadow: 0 4px 15px rgba(220, 38, 38, 0.4);
925
  }
926
  .product-list, .category-list {
927
  display: grid;
@@ -930,37 +1234,88 @@ def admin():
930
  .product-item, .category-item {
931
  background: #fff;
932
  padding: 20px;
933
- border-radius: 15px;
934
- box-shadow: 0 4px 15px rgba(0,0,0,0.1);
935
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
936
  .edit-form {
937
- margin-top: 15px;
938
- padding: 15px;
939
- background: #f7fafc;
940
  border-radius: 10px;
 
941
  }
 
 
 
942
  .color-input-group {
943
  display: flex;
944
  gap: 10px;
945
- margin-top: 5px;
946
  align-items: center;
947
  }
948
  .color-input-group input {
949
  flex-grow: 1;
950
- }
 
 
951
  .remove-color-btn {
952
- background-color: #ef4444;
953
  padding: 8px 12px;
954
  font-size: 0.8rem;
955
- margin-top: 5px;
956
  }
 
 
 
957
  .add-color-btn {
958
- background-color: #10b981;
959
- margin-bottom: 10px;
 
960
  }
961
  .add-color-btn:hover {
962
- background-color: #059669;
963
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
964
  </style>
965
  </head>
966
  <body>
@@ -969,49 +1324,55 @@ def admin():
969
  <img src="''' + LOGO_URL + '''" alt="Logo" class="header-logo">
970
  <h1>Админ-панель</h1>
971
  </div>
972
- <h1>Добавление товара</h1>
 
973
  <form method="POST" enctype="multipart/form-data">
974
  <input type="hidden" name="action" value="add">
975
- <label>Название товара:</label>
976
- <input type="text" name="name" required>
977
- <label>Цена:</label>
978
- <input type="number" name="price" step="0.01" required>
979
- <label>Описание:</label>
980
- <textarea name="description" rows="4" required></textarea>
981
- <label>Категория:</label>
982
- <select name="category">
 
 
 
983
  <option value="Без категории">Без категории</option>
984
  {% for category in categories %}
985
  <option value="{{ category }}">{{ category }}</option>
986
  {% endfor %}
987
  </select>
988
- <label>Фотографии (до 10):</label>
989
- <input type="file" name="photos" accept="image/*" multiple>
 
 
990
  <label>Цвета:</label>
991
  <div id="color-inputs-add">
992
  <div class="color-input-group">
993
  <input type="text" name="colors" placeholder="Например: Красный">
994
- <button type="button" class="remove-color-btn" onclick="this.parentElement.remove()">Удалить</button>
995
  </div>
996
  </div>
997
  <button type="button" class="add-color-btn" onclick="addColorInput('color-inputs-add')">Добавить цвет</button>
 
998
  <button type="submit">Добавить товар</button>
999
  </form>
1000
 
1001
- <h1>Управление категориями</h1>
1002
  <form method="POST">
1003
  <input type="hidden" name="action" value="add_category">
1004
- <label>Название категории:</label>
1005
- <input type="text" name="category_name" required>
1006
- <button type="submit">Добавить</button>
1007
  </form>
1008
 
1009
- <h2>Список категорий</h2>
1010
  <div class="category-list">
1011
  {% for category in categories %}
1012
  <div class="category-item">
1013
- <h3>{{ category }}</h3>
1014
- <form method="POST" style="display: inline;">
1015
  <input type="hidden" name="action" value="delete_category">
1016
  <input type="hidden" name="category_index" value="{{ loop.index0 }}">
1017
  <button type="submit" class="delete-button">Удалить</button>
@@ -1020,13 +1381,14 @@ def admin():
1020
  {% endfor %}
1021
  </div>
1022
 
1023
- <h2>Управление базой данных</h2>
1024
- <form method="POST" action="{{ url_for('backup') }}" style="display: inline;">
1025
- <button type="submit">Создать копию</button>
1026
- </form>
1027
- <form method="GET" action="{{ url_for('download') }}" style="display: inline;">
1028
- <button type="submit">Скачать базу</button>
1029
- </form>
 
1030
 
1031
  <h2>Список товаров</h2>
1032
  <div class="product-list">
@@ -1034,15 +1396,14 @@ def admin():
1034
  <div class="product-item">
1035
  <h3>{{ product['name'] }}</h3>
1036
  <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
1037
- <p><strong>Цена:</strong> {{ product['price'] }} с</p>
1038
- <p><strong>Описание:</strong> {{ product['description'] }}</p>
1039
- <p><strong>Цвета:</strong> {{ product.get('colors', ['Нет цветов'])|join(', ') }}</p>
1040
  {% if product.get('photos') and product['photos']|length > 0 %}
1041
- <div style="display: flex; flex-wrap: wrap; gap: 10px; margin-bottom:10px;">
1042
  {% for photo in product['photos'] %}
1043
  <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo }}"
1044
- alt="{{ product['name'] }}"
1045
- style="max-width: 100px; border-radius: 10px;">
1046
  {% endfor %}
1047
  </div>
1048
  {% endif %}
@@ -1053,7 +1414,7 @@ def admin():
1053
  <input type="hidden" name="index" value="{{ loop.index0 }}">
1054
  <label>Название:</label>
1055
  <input type="text" name="name" value="{{ product['name'] }}" required>
1056
- <label>Цена:</label>
1057
  <input type="number" name="price" step="0.01" value="{{ product['price'] }}" required>
1058
  <label>Описание:</label>
1059
  <textarea name="description" rows="4" required>{{ product['description'] }}</textarea>
@@ -1074,7 +1435,7 @@ def admin():
1074
  <button type="button" class="remove-color-btn" onclick="this.parentElement.remove()">Удалить</button>
1075
  </div>
1076
  {% endfor %}
1077
- {% if not product.get('colors') %}
1078
  <div class="color-input-group">
1079
  <input type="text" name="colors" placeholder="Например: Синий">
1080
  <button type="button" class="remove-color-btn" onclick="this.parentElement.remove()">Удалить</button>
@@ -1082,13 +1443,13 @@ def admin():
1082
  {% endif %}
1083
  </div>
1084
  <button type="button" class="add-color-btn" onclick="addColorInput('edit-color-inputs-{{ loop.index0 }}')">Добавить цвет</button>
1085
- <button type="submit">Сохранить</button>
1086
  </form>
1087
  </details>
1088
- <form method="POST" style="display: inline;">
1089
  <input type="hidden" name="action" value="delete">
1090
  <input type="hidden" name="index" value="{{ loop.index0 }}">
1091
- <button type="submit" class="delete-button">Удалить</button>
1092
  </form>
1093
  </div>
1094
  {% endfor %}
@@ -1099,12 +1460,12 @@ def admin():
1099
  const container = document.getElementById(containerId);
1100
  const newInputGroup = document.createElement('div');
1101
  newInputGroup.className = 'color-input-group';
1102
-
1103
  const newInput = document.createElement('input');
1104
  newInput.type = 'text';
1105
  newInput.name = 'colors';
1106
  newInput.placeholder = 'Например: Красный';
1107
-
1108
  const removeBtn = document.createElement('button');
1109
  removeBtn.type = 'button';
1110
  removeBtn.className = 'remove-color-btn';
@@ -1112,11 +1473,27 @@ def admin():
1112
  removeBtn.onclick = function() {
1113
  newInputGroup.remove();
1114
  };
1115
-
1116
  newInputGroup.appendChild(newInput);
1117
  newInputGroup.appendChild(removeBtn);
1118
  container.appendChild(newInputGroup);
1119
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1120
  </script>
1121
  </body>
1122
  </html>
@@ -1125,13 +1502,25 @@ def admin():
1125
 
1126
  @app.route('/backup', methods=['POST'])
1127
  def backup():
1128
- upload_db_to_hf()
1129
- return "Резервная копия создана.", 200
 
 
 
 
 
 
1130
 
1131
  @app.route('/download', methods=['GET'])
1132
  def download():
1133
- download_db_from_hf()
1134
- return "База данных скачана.", 200
 
 
 
 
 
 
1135
 
1136
  if __name__ == '__main__':
1137
  if HF_TOKEN_WRITE and REPO_ID:
@@ -1140,5 +1529,5 @@ if __name__ == '__main__':
1140
  try:
1141
  load_data()
1142
  except Exception as e:
1143
- logging.error(f"Не удалось загрузить базу данных: {e}")
1144
  app.run(debug=True, host='0.0.0.0', port=7860)
 
25
  download_db_from_hf()
26
  with open(DATA_FILE, 'r', encoding='utf-8') as file:
27
  data = json.load(file)
 
28
  if not isinstance(data, dict) or 'products' not in data or 'categories' not in data:
29
  return {'products': [], 'categories': [] if not isinstance(data, list) else data}
30
  return data
31
  except FileNotFoundError:
 
32
  return {'products': [], 'categories': []}
33
  except json.JSONDecodeError:
 
34
  return {'products': [], 'categories': []}
35
  except RepositoryNotFoundError:
 
36
  return {'products': [], 'categories': []}
37
  except Exception as e:
38
  logging.error(f"Произошла ошибка при загрузке данных: {e}")
 
42
  try:
43
  with open(DATA_FILE, 'w', encoding='utf-8') as file:
44
  json.dump(data, file, ensure_ascii=False, indent=4)
 
45
  upload_db_to_hf()
46
  except Exception as e:
47
  logging.error(f"Ошибка при сохранении данных: {e}")
 
58
  token=HF_TOKEN_WRITE,
59
  commit_message=f"Автоматическое резервное копирование базы данных {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
60
  )
 
61
  except Exception as e:
62
  logging.error(f"Ошибка при загрузке резервной копии: {e}")
63
 
 
71
  local_dir=".",
72
  local_dir_use_symlinks=False
73
  )
 
74
  except RepositoryNotFoundError as e:
75
  logging.error(f"Репозиторий не найден: {e}")
76
  raise
 
88
  data = load_data()
89
  products = data['products']
90
  categories = data['categories']
91
+
92
  catalog_html = '''
93
  <!DOCTYPE html>
94
  <html lang="ru">
95
  <head>
96
  <meta charset="UTF-8">
97
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
98
+ <title>Zalkar Textile - Изысканные ткани на заказ</title>
99
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
100
+ <link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;700&family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
101
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.css">
102
  <style>
103
+ :root {
104
+ --primary-color: #5a6f85;
105
+ --secondary-color: #95afba;
106
+ --accent-color: #e6b980;
107
+ --text-color: #333;
108
+ --background-color: #f8f5f1;
109
+ --card-background: #fff;
110
+ --border-color: #e0d8ce;
111
+ --price-color: #c0392b; /* A warm red for price */
112
+ --button-bg: var(--primary-color);
113
+ --button-hover-bg: #4a5e73;
114
+ --button-text: #fff;
115
+ --cart-button-bg: var(--price-color);
116
+ --cart-button-hover-bg: #a93226;
117
+ --add-to-cart-bg: #27ae60; /* Green for add to cart */
118
+ --add-to-cart-hover-bg: #229954;
119
+ --delete-button-bg: var(--price-color);
120
+ --delete-button-hover-bg: #a93226;
121
+ }
122
+
123
+ body.dark-mode {
124
+ --primary-color: #c7d0d8;
125
+ --secondary-color: #aebfd0;
126
+ --accent-color: #f1d4b0;
127
+ --text-color: #e0e0e0;
128
+ --background-color: #1a202c;
129
+ --card-background: #2d3748;
130
+ --border-color: #4a5568;
131
+ --price-color: #f1c40f; /* Gold for price in dark mode */
132
+ --button-bg: var(--accent-color);
133
+ --button-hover-bg: #d4a970;
134
+ --button-text: #2d3748;
135
+ --cart-button-bg: var(--price-color);
136
+ --cart-button-hover-bg: #d4ac0d;
137
+ --add-to-cart-bg: #2ecc71;
138
+ --add-to-cart-hover-bg: #27ae60;
139
+ --delete-button-bg: var(--price-color);
140
+ --delete-button-hover-bg: #d4ac0d;
141
+ }
142
+
143
  * {
144
  margin: 0;
145
  padding: 0;
146
  box-sizing: border-box;
147
  }
148
  body {
149
+ font-family: 'Roboto', sans-serif;
150
+ background: var(--background-color);
151
+ color: var(--text-color);
152
  line-height: 1.6;
153
+ transition: background 0.5s, color 0.5s;
 
 
 
 
154
  }
155
  .container {
156
+ max-width: 1400px;
157
  margin: 0 auto;
158
  padding: 20px;
159
  }
 
162
  justify-content: space-between;
163
  align-items: center;
164
  padding: 15px 0;
165
+ border-bottom: 1px solid var(--border-color);
166
+ margin-bottom: 30px;
167
+ }
168
+ .header-left {
169
+ display: flex;
170
+ align-items: center;
171
  }
172
  .header-logo {
173
  width: 60px;
 
182
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
183
  }
184
  .header h1 {
185
+ font-family: 'Playfair Display', serif;
186
+ font-size: 2rem;
187
+ font-weight: 700;
188
  margin-left: 15px;
189
+ color: var(--primary-color);
190
  }
191
+ body.dark-mode .header h1 {
192
+ color: var(--accent-color);
193
+ }
194
  .theme-toggle {
195
  background: none;
196
  border: none;
197
  font-size: 1.5rem;
198
  cursor: pointer;
199
+ color: var(--primary-color);
200
  transition: color 0.3s ease;
201
  }
202
  .theme-toggle:hover {
203
+ color: var(--accent-color);
204
  }
205
  .filters-container {
206
  margin: 20px 0;
 
215
  }
216
  #search-input {
217
  width: 90%;
218
+ max-width: 700px;
219
+ padding: 12px 20px;
220
+ font-size: 1.1rem;
221
+ border: 1px solid var(--border-color);
222
+ border-radius: 25px;
223
  outline: none;
224
+ box-shadow: 0 2px 8px rgba(0,0,0,0.08);
225
  transition: all 0.3s ease;
226
+ background: var(--card-background);
227
+ color: var(--text-color);
228
  }
229
  #search-input:focus {
230
+ border-color: var(--primary-color);
231
+ box-shadow: 0 4px 15px rgba(90, 111, 133, 0.2);
232
  }
233
  .category-filter {
234
+ padding: 10px 20px;
235
+ border: 1px solid var(--border-color);
236
+ border-radius: 20px;
237
+ background-color: var(--card-background);
238
  cursor: pointer;
239
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
240
  font-size: 0.9rem;
241
  font-weight: 400;
242
+ color: var(--text-color);
243
  }
244
  .category-filter.active, .category-filter:hover {
245
+ background-color: var(--primary-color);
246
+ color: var(--button-text);
247
+ border-color: var(--primary-color);
248
+ box-shadow: 0 4px 12px rgba(90, 111, 133, 0.3);
249
+ }
250
+ body.dark-mode .category-filter {
251
+ background-color: var(--card-background);
252
+ color: var(--text-color);
253
+ border-color: var(--border-color);
254
+ }
255
+ body.dark-mode .category-filter.active, body.dark-mode .category-filter:hover {
256
+ background-color: var(--accent-color);
257
+ color: var(--button-text);
258
+ border-color: var(--accent-color);
259
+ }
260
  .products-grid {
261
  display: grid;
262
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
263
+ gap: 25px;
264
  padding: 10px;
265
  }
266
  .product {
267
+ background: var(--card-background);
268
  border-radius: 15px;
269
+ padding: 20px;
270
+ box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
271
+ transition: transform 0.4s cubic-bezier(0.2, 0.8, 0.2, 1), box-shadow 0.4s ease;
272
  overflow: hidden;
273
+ display: flex;
274
+ flex-direction: column;
 
 
275
  }
276
  .product:hover {
277
+ transform: translateY(-8px) scale(1.03);
278
+ box-shadow: 0 12px 25px rgba(0, 0, 0, 0.15);
279
  }
280
  .product-image {
281
  width: 100%;
282
  aspect-ratio: 1;
283
+ background-color: #f0f0f0;
284
  border-radius: 10px;
285
  overflow: hidden;
286
  display: flex;
287
  justify-content: center;
288
  align-items: center;
289
+ margin-bottom: 15px;
290
+ position: relative;
291
+ }
292
+ body.dark-mode .product-image {
293
+ background-color: #4a5568;
294
  }
295
  .product-image img {
296
+ width: 100%;
297
+ height: 100%;
298
+ object-fit: cover;
299
+ transition: transform 0.5s ease;
300
  }
301
  .product-image img:hover {
302
  transform: scale(1.1);
303
  }
304
  .product h2 {
305
+ font-family: 'Playfair Display', serif;
306
+ font-size: 1.2rem;
307
+ font-weight: 700;
308
+ margin: 5px 0;
309
  text-align: center;
310
+ color: var(--primary-color);
311
  white-space: nowrap;
312
  overflow: hidden;
313
  text-overflow: ellipsis;
314
  }
315
+ body.dark-mode .product h2 {
316
+ color: var(--accent-color);
317
+ }
318
  .product-price {
319
+ font-size: 1.3rem;
320
+ color: var(--price-color);
321
  font-weight: 700;
322
  text-align: center;
323
  margin: 5px 0;
324
  }
325
  .product-description {
326
+ font-size: 0.9rem;
327
+ color: var(--text-color);
328
  text-align: center;
329
  margin-bottom: 15px;
330
  overflow: hidden;
331
  text-overflow: ellipsis;
332
+ display: -webkit-box;
333
+ -webkit-line-clamp: 2;
334
+ -webkit-box-orient: vertical;
335
+ flex-grow: 1;
336
  }
337
+ .product-actions {
338
+ display: flex;
339
+ gap: 10px;
340
+ margin-top: auto;
341
  }
342
  .product-button {
343
  display: block;
344
  width: 100%;
345
+ padding: 10px;
346
  border: none;
347
  border-radius: 8px;
348
+ background-color: var(--button-bg);
349
+ color: var(--button-text);
350
+ font-size: 0.9rem;
351
  font-weight: 500;
352
  cursor: pointer;
353
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
 
354
  text-align: center;
355
  text-decoration: none;
356
+ flex-grow: 1;
357
  }
358
  .product-button:hover {
359
+ background-color: var(--button-hover-bg);
360
+ box-shadow: 0 4px 12px rgba(90, 111, 133, 0.4);
361
  transform: translateY(-2px);
362
  }
363
  .add-to-cart {
364
+ background-color: var(--add-to-cart-bg);
365
  }
366
  .add-to-cart:hover {
367
+ background-color: var(--add-to-cart-hover-bg);
368
+ box-shadow: 0 4px 12px rgba(39, 174, 96, 0.4);
369
  }
370
  #cart-button {
371
  position: fixed;
372
+ bottom: 30px;
373
+ right: 30px;
374
+ background-color: var(--cart-button-bg);
375
  color: white;
376
  border: none;
377
  border-radius: 50%;
378
+ width: 60px;
379
+ height: 60px;
380
+ font-size: 1.5rem;
381
  cursor: pointer;
382
  display: none;
383
+ box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2);
384
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
385
  z-index: 1000;
386
+ display: flex;
387
+ align-items: center;
388
+ justify-content: center;
389
  }
390
+ #cart-button:hover {
391
+ background-color: var(--cart-button-hover-bg);
392
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
393
+ transform: scale(1.1);
394
+ }
395
+
396
  .modal {
397
  display: none;
398
  position: fixed;
 
401
  top: 0;
402
  width: 100%;
403
  height: 100%;
404
+ background-color: rgba(0,0,0,0.6);
405
+ backdrop-filter: blur(8px);
406
+ overflow: auto;
407
+ padding-top: 50px;
408
  }
409
  .modal-content {
410
+ background: var(--card-background);
411
  margin: 5% auto;
412
+ padding: 30px;
413
+ border-radius: 20px;
414
+ width: 95%;
415
+ max-width: 800px;
416
+ box-shadow: 0 15px 40px rgba(0,0,0,0.3);
417
+ animation: slideIn 0.4s ease-out;
 
 
 
 
418
  }
419
  @keyframes slideIn {
420
+ from { transform: translateY(-80px); opacity: 0; }
421
  to { transform: translateY(0); opacity: 1; }
422
  }
423
  .close {
424
  float: right;
425
+ font-size: 2rem;
426
+ font-weight: bold;
427
+ color: var(--text-color);
428
  cursor: pointer;
429
  transition: color 0.3s;
430
+ margin-top: -15px;
431
+ margin-right: -15px;
432
  }
433
  .close:hover {
434
+ color: var(--price-color);
 
 
 
 
 
 
435
  }
436
  .cart-item {
437
  display: flex;
438
  justify-content: space-between;
439
  align-items: center;
440
  padding: 15px 0;
441
+ border-bottom: 1px solid var(--border-color);
442
  }
443
+ .cart-item:last-child {
444
+ border-bottom: none;
445
  }
446
+ .cart-item-details {
447
+ display: flex;
448
+ align-items: center;
449
+ }
450
+ .cart-item-details img {
451
+ width: 70px;
452
+ height: 70px;
453
+ object-fit: cover;
454
+ border-radius: 10px;
455
  margin-right: 15px;
456
+ border: 1px solid var(--border-color);
457
  }
458
+ .cart-item-details strong {
459
+ font-size: 1.1rem;
460
+ color: var(--primary-color);
461
+ }
462
+ .cart-item-details p {
463
+ font-size: 0.9rem;
464
+ color: var(--text-color);
465
+ }
466
+ .cart-item span {
467
+ font-size: 1.1rem;
468
+ font-weight: 600;
469
+ color: var(--price-color);
470
+ }
471
+
472
+ .quantity-modal-content {
473
+ text-align: center;
474
+ }
475
+ .quantity-modal-content h2 {
476
+ font-family: 'Playfair Display', serif;
477
+ color: var(--primary-color);
478
+ margin-bottom: 20px;
479
+ }
480
  .quantity-input, .color-select {
481
  width: 100%;
482
+ max-width: 200px;
483
+ padding: 12px;
484
+ border: 1px solid var(--border-color);
485
  border-radius: 8px;
486
  font-size: 1rem;
487
+ margin: 10px auto;
488
+ display: block;
489
+ background: var(--background-color);
490
+ color: var(--text-color);
491
+ }
492
+ .quantity-input:focus, .color-select:focus {
493
+ border-color: var(--primary-color);
494
+ box-shadow: 0 0 5px rgba(90, 111, 133, 0.3);
495
+ outline: none;
496
+ }
497
+
498
+ .modal-footer {
499
+ margin-top: 30px;
500
+ text-align: right;
501
+ border-top: 1px solid var(--border-color);
502
+ padding-top: 20px;
503
  }
504
+ .modal-footer strong {
505
+ font-size: 1.4rem;
506
+ color: var(--primary-color);
507
+ }
508
+ .modal-footer button {
509
+ margin-left: 10px;
510
+ padding: 10px 20px;
511
+ font-size: 1rem;
512
+ }
513
+
514
  .clear-cart {
515
+ background-color: var(--delete-button-bg);
516
  }
517
  .clear-cart:hover {
518
+ background-color: var(--delete-button-hover-bg);
519
+ box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4);
520
  }
521
  .order-button {
522
+ background-color: var(--add-to-cart-bg);
523
  }
524
  .order-button:hover {
525
+ background-color: var(--add-to-cart-hover-bg);
526
+ box-shadow: 0 4px 12px rgba(39, 174, 96, 0.4);
527
+ }
528
+
529
+ /* Swiper Styles for Product Detail Modal */
530
+ .swiper-container {
531
+ width: 100%;
532
+ height: 350px;
533
+ border-radius: 15px;
534
+ margin-bottom: 20px;
535
+ box-shadow: 0 4px 15px rgba(0,0,0,0.1);
536
+ }
537
+ .swiper-slide {
538
+ display: flex;
539
+ justify-content: center;
540
+ align-items: center;
541
+ background: var(--background-color);
542
+ border-radius: 15px;
543
+ overflow: hidden;
544
+ }
545
+ body.dark-mode .swiper-slide {
546
+ background: #4a5568;
547
+ }
548
+ .swiper-slide img {
549
+ max-width: 100%;
550
+ max-height: 350px;
551
+ object-fit: contain;
552
+ }
553
+ .swiper-button-next, .swiper-button-prev {
554
+ color: var(--primary-color) !important;
555
+ }
556
+ body.dark-mode .swiper-button-next, body.dark-mode .swiper-button-prev {
557
+ color: var(--accent-color) !important;
558
+ }
559
+ .swiper-pagination-bullet {
560
+ background: var(--primary-color) !important;
561
+ }
562
+ body.dark-mode .swiper-pagination-bullet {
563
+ background: var(--accent-color) !important;
564
+ }
565
+ /* End Swiper Styles */
566
+
567
+ .product-detail-content p {
568
+ margin-bottom: 10px;
569
+ font-size: 1rem;
570
+ }
571
+ .product-detail-content strong {
572
+ color: var(--primary-color);
573
+ }
574
+ body.dark-mode .product-detail-content strong {
575
+ color: var(--accent-color);
576
+ }
577
+
578
+ @media (max-width: 768px) {
579
+ .header h1 {
580
+ font-size: 1.5rem;
581
+ }
582
+ .products-grid {
583
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
584
+ gap: 15px;
585
+ }
586
+ .product {
587
+ padding: 15px;
588
+ }
589
+ .product h2 {
590
+ font-size: 1rem;
591
+ }
592
+ .product-price {
593
+ font-size: 1.1rem;
594
+ }
595
+ .product-description {
596
+ font-size: 0.8rem;
597
+ }
598
+ .product-button {
599
+ padding: 8px;
600
+ font-size: 0.8rem;
601
+ }
602
+ #cart-button {
603
+ width: 50px;
604
+ height: 50px;
605
+ font-size: 1.2rem;
606
+ bottom: 20px;
607
+ right: 20px;
608
+ }
609
+ .modal-content {
610
+ padding: 20px;
611
+ }
612
+ .close {
613
+ font-size: 1.5rem;
614
+ margin-top: -10px;
615
+ margin-right: -10px;
616
+ }
617
+ .cart-item-details img {
618
+ width: 50px;
619
+ height: 50px;
620
+ margin-right: 10px;
621
+ }
622
+ .cart-item-details strong {
623
+ font-size: 1rem;
624
+ }
625
+ .cart-item-details p, .cart-item span {
626
+ font-size: 0.9rem;
627
+ }
628
+ .modal-footer strong {
629
+ font-size: 1.2rem;
630
+ }
631
+ .modal-footer button {
632
+ padding: 8px 15px;
633
+ font-size: 0.9rem;
634
+ }
635
+ .swiper-container {
636
+ height: 300px;
637
+ }
638
+ .swiper-slide img {
639
+ max-height: 300px;
640
+ }
641
  }
642
  </style>
643
  </head>
644
  <body>
645
  <div class="container">
646
  <div class="header">
647
+ <div class="header-left">
648
+ <img src="''' + LOGO_URL + '''" alt="Logo" class="header-logo">
649
+ <h1>Zalkar Textile</h1>
650
+ </div>
651
  <button class="theme-toggle" onclick="toggleTheme()">
652
  <i class="fas fa-moon"></i>
653
  </button>
 
659
  {% endfor %}
660
  </div>
661
  <div class="search-container">
662
+ <input type="text" id="search-input" placeholder="Поиск по названию или описанию...">
663
  </div>
664
  <div class="products-grid" id="products-grid">
665
  {% for product in products %}
666
+ <div class="product"
667
+ data-name="{{ product['name']|lower }}"
668
  data-description="{{ product['description']|lower }}"
669
  data-category="{{ product.get('category', 'Без категории') }}">
670
  {% if product.get('photos') and product['photos']|length > 0 %}
671
  <div class="product-image">
672
+ <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product['photos'][0] }}"
673
+ alt="{{ product['name'] }}"
674
  loading="lazy">
675
  </div>
676
+ {% else %}
677
+ <div class="product-image">
678
+ <img src="https://via.placeholder.com/300x300?text=No+Image" alt="No Image">
679
+ </div>
680
  {% endif %}
681
  <h2>{{ product['name'] }}</h2>
682
+ <div class="product-price">{{ "{:.2f}".format(product['price']) }} $</div>
683
+ <p class="product-description">{{ product['description'] }}</p>
684
+ <div class="product-actions">
685
+ <button class="product-button" onclick="openModal({{ loop.index0 }})">Подробнее</button>
686
+ <button class="product-button add-to-cart" onclick="openQuantityModal({{ loop.index0 }})">В корзину</button>
687
+ </div>
688
  </div>
689
  {% endfor %}
690
  </div>
 
698
  </div>
699
 
700
  <div id="quantityModal" class="modal">
701
+ <div class="modal-content quantity-modal-content">
702
  <span class="close" onclick="closeModal('quantityModal')">×</span>
703
  <h2>Укажите количество и цвет</h2>
704
  <input type="number" id="quantityInput" class="quantity-input" step="0.1" min="0.1" value="1">
705
  <select id="colorSelect" class="color-select"></select>
706
+ <button class="product-button" onclick="confirmAddToCart()">Добавить в корзину</button>
707
  </div>
708
  </div>
709
 
 
712
  <span class="close" onclick="closeModal('cartModal')">×</span>
713
  <h2>Корзина</h2>
714
  <div id="cartContent"></div>
715
+ <div class="modal-footer">
716
+ <strong>Итого: <span id="cartTotal">0.00</span> $</strong>
717
  <button class="product-button clear-cart" onclick="clearCart()">Очистить</button>
718
+ <button class="product-button order-button" onclick="orderViaWhatsApp()">Заказать через WhatsApp</button>
719
  </div>
720
  </div>
721
  </div>
722
 
723
  <button id="cart-button" onclick="openCartModal()">🛒</button>
724
 
 
 
725
  <script src="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.js"></script>
726
  <script>
727
  const products = {{ products|tojson }};
 
760
  }
761
 
762
  function initializeSwiper() {
763
+ setTimeout(() => {
764
+ new Swiper('.swiper-container', {
765
+ slidesPerView: 1,
766
+ spaceBetween: 20,
767
+ loop: true,
768
+ grabCursor: true,
769
+ pagination: { el: '.swiper-pagination', clickable: true },
770
+ navigation: { nextEl: '.swiper-button-next', prevEl: '.swiper-button-prev' },
771
+ zoom: { maxRatio: 3 }
772
+ });
773
+ }, 50); // Small delay to ensure Swiper container is visible and rendered
774
  }
775
 
776
  function openQuantityModal(index) {
 
787
  });
788
  } else {
789
  const option = document.createElement('option');
790
+ option.value = 'Без цвета';
791
+ option.text = 'Без цвета';
792
  colorSelect.appendChild(option);
793
  }
794
  document.getElementById('quantityModal').style.display = 'block';
 
830
 
831
  function updateCartButton() {
832
  const cart = JSON.parse(localStorage.getItem('cart') || '[]');
833
+ const cartButton = document.getElementById('cart-button');
834
+ if (cart.length > 0) {
835
+ cartButton.style.display = 'flex'; // Use flex to center content
836
+ } else {
837
+ cartButton.style.display = 'none';
838
+ }
839
  }
840
 
841
  function openCartModal() {
 
843
  const cartContent = document.getElementById('cartContent');
844
  let total = 0;
845
 
846
+ cartContent.innerHTML = cart.length === 0 ? '<p style="text-align: center;">Корзина пуста</p>' : cart.map(item => {
847
  const itemTotal = item.price * item.quantity;
848
  total += itemTotal;
849
+ const photoUrl = item.photo ? `https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/${item.photo}` : 'https://via.placeholder.com/70x70?text=No+Image';
850
  return `
851
  <div class="cart-item">
852
+ <div class="cart-item-details">
853
+ <img src="${photoUrl}" alt="${item.name}">
854
  <div>
855
  <strong>${item.name}</strong>
856
+ <p>${item.price.toFixed(2)} $ × ${item.quantity.toFixed(2)} ${item.color !== 'Без цвета' ? `(Цвет: ${item.color})` : ''}</p>
857
  </div>
858
  </div>
859
+ <span>${itemTotal.toFixed(2)} $</span>
860
  </div>
861
  `;
862
  }).join('');
 
876
  cart.forEach((item, index) => {
877
  const itemTotal = item.price * item.quantity;
878
  total += itemTotal;
879
+ orderText += `${index + 1}. ${item.name} - ${item.price.toFixed(2)} $ × ${item.quantity.toFixed(2)} ${item.color !== 'Без цвета' ? `(Цвет: ${item.color})` : ''}%0A`;
880
  });
881
+ orderText += `Итого: ${total.toFixed(2)} $`;
882
  window.open(`https://api.whatsapp.com/send?phone=996707842888&text=${orderText}`, '_blank');
883
  }
884
 
885
  function clearCart() {
886
  localStorage.removeItem('cart');
887
+ openCartModal(); // Refresh the cart display
888
  updateCartButton();
889
  }
890
 
891
  window.onclick = function(event) {
892
+ if (event.target.classList.contains('modal')) {
893
+ event.target.style.display = "none";
894
+ }
895
  }
896
 
897
  document.getElementById('search-input').addEventListener('input', filterProducts);
 
912
  const category = product.getAttribute('data-category');
913
  const matchesSearch = name.includes(searchTerm) || description.includes(searchTerm);
914
  const matchesCategory = activeCategory === 'all' || category === activeCategory;
915
+ product.style.display = matchesSearch && matchesCategory ? 'flex' : 'none'; // Use flex because .product is flex
916
  });
917
  }
918
 
 
932
  except IndexError:
933
  return "Продукт не найден", 404
934
  detail_html = '''
935
+ <div class="product-detail-content">
936
+ <h2 style="font-family: 'Playfair Display', serif; font-size: 1.8rem; font-weight: 700; margin-bottom: 20px; color: var(--primary-color);">{{ product['name'] }}</h2>
937
+ <div class="swiper-container" style="max-width: 500px; margin: 0 auto 20px;">
938
  <div class="swiper-wrapper">
939
  {% if product.get('photos') %}
940
  {% for photo in product['photos'] %}
941
+ <div class="swiper-slide">
942
  <div class="swiper-zoom-container">
943
+ <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo }}"
944
+ alt="{{ product['name'] }}"
945
+ style="max-width: 100%; max-height: 400px; object-fit: contain;">
946
  </div>
947
  </div>
948
  {% endfor %}
949
  {% else %}
950
  <div class="swiper-slide">
951
+ <img src="https://via.placeholder.com/400x400?text=No+Image" alt="No Image">
952
  </div>
953
  {% endif %}
954
  </div>
 
957
  <div class="swiper-button-prev"></div>
958
  </div>
959
  <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
960
+ <p><strong>Цена:</strong> {{ "{:.2f}".format(product['price']) }} $</p>
961
  <p><strong>Описание:</strong> {{ product['description'] }}</p>
962
+ <p><strong>Доступные цвета:</strong> {{ product.get('colors', ['Без цветов'])|join(', ') }}</p>
963
  </div>
964
  '''
965
  return render_template_string(detail_html, product=product, repo_id=REPO_ID)
 
972
 
973
  if request.method == 'POST':
974
  action = request.form.get('action')
975
+
976
  if action == 'add_category':
977
  category_name = request.form.get('category_name')
978
  if category_name and category_name not in categories:
 
982
  return "Ошибка: Категория уже существует или не указано название", 400
983
 
984
  elif action == 'delete_category':
985
+ try:
986
+ category_index = int(request.form.get('category_index'))
987
+ if 0 <= category_index < len(categories):
988
+ deleted_category = categories.pop(category_index)
989
+ for product in products:
990
+ if product.get('category') == deleted_category:
991
+ product['category'] = 'Без категории'
992
+ save_data(data)
993
+ return redirect(url_for('admin'))
994
+ return "Ошибка: Недопустимый индекс категории", 400
995
+ except ValueError:
996
+ return "Ошибка: Недопустимый индекс категории", 400
997
+
998
 
999
  elif action == 'add':
1000
  name = request.form.get('name')
 
1004
  photos_files = request.files.getlist('photos')
1005
  colors = request.form.getlist('colors')
1006
  photos_list = []
1007
+
1008
  if photos_files:
1009
+ api = HfApi()
1010
+ uploads_dir = 'uploads'
1011
+ os.makedirs(uploads_dir, exist_ok=True)
1012
  for photo in photos_files[:10]:
1013
  if photo and photo.filename:
1014
  photo_filename = secure_filename(photo.filename)
 
 
1015
  temp_path = os.path.join(uploads_dir, photo_filename)
1016
  photo.save(temp_path)
1017
+ try:
1018
+ api.upload_file(
1019
+ path_or_fileobj=temp_path,
1020
+ path_in_repo=f"photos/{photo_filename}",
1021
+ repo_id=REPO_ID,
1022
+ repo_type="dataset",
1023
+ token=HF_TOKEN_WRITE,
1024
+ commit_message=f"Добавлено фото для товара {name}"
1025
+ )
1026
+ photos_list.append(photo_filename)
1027
+ except Exception as e:
1028
+ logging.error(f"Ошибка при загрузке фото {photo_filename} на HF: {e}")
1029
+ finally:
1030
+ if os.path.exists(temp_path):
1031
+ os.remove(temp_path)
1032
+
1033
  if not name or not price or not description:
1034
  return "Ошибка: Заполните все обязательные поля", 400
1035
+
1036
+ try:
1037
+ price = float(price.replace(',', '.'))
1038
+ except ValueError:
1039
+ return "Ошибка: Недопустимое значение цены", 400
1040
+
1041
  new_product = {
1042
  'name': name,
1043
  'price': price,
1044
  'description': description,
1045
  'category': category if category in categories else 'Без категории',
1046
  'photos': photos_list,
1047
+ 'colors': [c.strip() for c in colors if c.strip()] if colors else []
1048
  }
1049
  products.append(new_product)
1050
  save_data(data)
1051
  return redirect(url_for('admin'))
1052
+
1053
  elif action == 'edit':
1054
+ try:
1055
+ index = int(request.form.get('index'))
1056
+ if not (0 <= index < len(products)):
1057
+ return "Ошибка: Недопустимый индекс товара", 400
1058
+
1059
+ name = request.form.get('name')
1060
+ price = request.form.get('price')
1061
+ description = request.form.get('description')
1062
+ category = request.form.get('category')
1063
+ photos_files = request.files.getlist('photos')
1064
+ colors = request.form.getlist('colors')
1065
+
1066
+ if not name or not price or not description:
1067
+ return "Ошибка: Заполните все обязательные поля", 400
1068
+
1069
+ try:
1070
+ price = float(price.replace(',', '.'))
1071
+ except ValueError:
1072
+ return "Ошибка: Недопустимое значение цены", 400
1073
+
1074
+ products[index]['name'] = name
1075
+ products[index]['price'] = price
1076
+ products[index]['description'] = description
1077
+ products[index]['category'] = category if category in categories else 'Без категории'
1078
+ products[index]['colors'] = [c.strip() for c in colors if c.strip()] if colors else []
1079
+
1080
+ if photos_files and any(photo.filename for photo in photos_files):
1081
+ new_photos_list = []
1082
+ api = HfApi()
1083
+ uploads_dir = 'uploads'
1084
+ os.makedirs(uploads_dir, exist_ok=True)
1085
+ for photo in photos_files[:10]:
1086
+ if photo and photo.filename:
1087
+ photo_filename = secure_filename(photo.filename)
1088
+ temp_path = os.path.join(uploads_dir, photo_filename)
1089
+ photo.save(temp_path)
1090
+ try:
1091
+ api.upload_file(
1092
+ path_or_fileobj=temp_path,
1093
+ path_in_repo=f"photos/{photo_filename}",
1094
+ repo_id=REPO_ID,
1095
+ repo_type="dataset",
1096
+ token=HF_TOKEN_WRITE,
1097
+ commit_message=f"Обновлено фото для товара {name}"
1098
+ )
1099
+ new_photos_list.append(photo_filename)
1100
+ except Exception as e:
1101
+ logging.error(f"Ошибка при загрузке фото {photo_filename} на HF: {e}")
1102
+ finally:
1103
+ if os.path.exists(temp_path):
1104
+ os.remove(temp_path)
1105
+ products[index]['photos'] = new_photos_list
1106
+ elif 'photos' not in products[index]:
1107
+ products[index]['photos'] = []
1108
+
1109
+
1110
+ save_data(data)
1111
+ return redirect(url_for('admin'))
1112
+ except ValueError:
1113
+ return "Ошибка: Недопустимый индекс товара", 400
1114
+
1115
  elif action == 'delete':
1116
+ try:
1117
+ index = int(request.form.get('index'))
1118
+ if 0 <= index < len(products):
1119
+ del products[index]
1120
+ save_data(data)
1121
+ return redirect(url_for('admin'))
1122
+ return "Ошибка: Недопустимый индекс товара", 400
1123
+ except ValueError:
1124
+ return "Ошибка: Недопустимый индекс товара", 400
1125
+
1126
  admin_html = '''
1127
  <!DOCTYPE html>
1128
  <html lang="ru">
 
1130
  <meta charset="UTF-8">
1131
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
1132
  <title>Админ-панель</title>
1133
+ <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
1134
  <style>
1135
  body {
1136
+ font-family: 'Roboto', sans-serif;
1137
  background: linear-gradient(135deg, #f0f2f5, #e9ecef);
1138
+ color: #333;
1139
  padding: 20px;
1140
+ line-height: 1.6;
1141
  }
1142
  .container {
1143
  max-width: 1200px;
1144
  margin: 0 auto;
1145
+ background: #fff;
1146
+ padding: 30px;
1147
+ border-radius: 15px;
1148
+ box-shadow: 0 8px 20px rgba(0,0,0,0.1);
1149
  }
1150
  .header {
1151
  display: flex;
1152
  align-items: center;
1153
+ padding-bottom: 20px;
1154
+ border-bottom: 1px solid #e0e0e0;
1155
+ margin-bottom: 30px;
1156
  }
1157
  .header-logo {
1158
+ width: 50px;
1159
+ height: 50px;
1160
  border-radius: 50%;
1161
  object-fit: cover;
 
 
1162
  margin-right: 15px;
1163
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
 
 
 
1164
  }
1165
  h1, h2 {
1166
+ font-weight: 700;
1167
  margin-bottom: 20px;
1168
+ color: #5a6f85;
1169
  }
1170
+ h2 {
1171
+ margin-top: 30px;
1172
+ padding-top: 20px;
1173
+ border-top: 1px solid #e0e0e0;
1174
+ }
1175
  form {
 
 
 
 
1176
  margin-bottom: 30px;
1177
+ padding: 20px;
1178
+ background: #f7f9fc;
1179
+ border-radius: 10px;
1180
+ box-shadow: 0 2px 8px rgba(0,0,0,0.05);
1181
  }
1182
  label {
1183
  font-weight: 500;
1184
  margin-top: 15px;
1185
  display: block;
1186
+ margin-bottom: 5px;
1187
  }
1188
+ input[type="text"], input[type="number"], textarea, select, input[type="file"] {
1189
  width: 100%;
1190
+ padding: 10px;
1191
+ border: 1px solid #ccc;
1192
+ border-radius: 5px;
 
1193
  font-size: 1rem;
1194
+ transition: border-color 0.3s ease;
1195
  box-sizing: border-box;
1196
  }
1197
+ input[type="file"] {
1198
+ padding: 10px 5px; /* Adjust padding for file input */
1199
+ }
1200
  input:focus, textarea:focus, select:focus {
1201
+ border-color: #5a6f85;
1202
+ box-shadow: 0 0 5px rgba(90, 111, 133, 0.2);
1203
  outline: none;
1204
  }
1205
  button {
1206
+ padding: 10px 20px;
1207
  border: none;
1208
+ border-radius: 5px;
1209
+ background-color: #5a6f85;
1210
  color: white;
1211
  font-weight: 500;
1212
  cursor: pointer;
1213
+ transition: background-color 0.3s ease, box-shadow 0.3s ease;
1214
  margin-top: 15px;
1215
+ margin-right: 10px;
1216
+ }
1217
+ button:last-child {
1218
+ margin-right: 0;
1219
  }
1220
  button:hover {
1221
+ background-color: #4a5e73;
1222
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
 
1223
  }
1224
  .delete-button {
1225
+ background-color: #e74c3c;
1226
  }
1227
  .delete-button:hover {
1228
+ background-color: #c0392b;
 
1229
  }
1230
  .product-list, .category-list {
1231
  display: grid;
 
1234
  .product-item, .category-item {
1235
  background: #fff;
1236
  padding: 20px;
1237
+ border-radius: 10px;
1238
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
1239
+ border: 1px solid #eee;
1240
+ }
1241
+ .category-item {
1242
+ display: flex;
1243
+ justify-content: space-between;
1244
+ align-items: center;
1245
+ }
1246
+ .category-item form {
1247
+ margin: 0;
1248
+ padding: 0;
1249
+ box-shadow: none;
1250
+ background: none;
1251
+ }
1252
+ .product-item h3 {
1253
+ margin-top: 0;
1254
+ margin-bottom: 10px;
1255
+ color: #5a6f85;
1256
+ }
1257
+ .product-item p {
1258
+ margin-bottom: 8px;
1259
+ font-size: 0.95rem;
1260
+ color: #555;
1261
+ }
1262
+ .product-item p strong {
1263
+ color: #333;
1264
+ }
1265
  .edit-form {
1266
+ margin-top: 20px;
1267
+ padding: 20px;
1268
+ background: #f0f2f5;
1269
  border-radius: 10px;
1270
+ box-shadow: none;
1271
  }
1272
+ .edit-form button {
1273
+ margin-top: 10px;
1274
+ }
1275
  .color-input-group {
1276
  display: flex;
1277
  gap: 10px;
1278
+ margin-bottom: 10px;
1279
  align-items: center;
1280
  }
1281
  .color-input-group input {
1282
  flex-grow: 1;
1283
+ margin-top: 0;
1284
+ margin-bottom: 0;
1285
+ }
1286
  .remove-color-btn {
1287
+ background-color: #e74c3c;
1288
  padding: 8px 12px;
1289
  font-size: 0.8rem;
1290
+ margin-top: 0;
1291
  }
1292
+ .remove-color-btn:hover {
1293
+ background-color: #c0392b;
1294
+ }
1295
  .add-color-btn {
1296
+ background-color: #2ecc71;
1297
+ margin-top: 0;
1298
+ margin-bottom: 15px;
1299
  }
1300
  .add-color-btn:hover {
1301
+ background-color: #27ae60;
1302
  }
1303
+ .product-photos-preview {
1304
+ display: flex;
1305
+ flex-wrap: wrap;
1306
+ gap: 10px;
1307
+ margin-top: 10px;
1308
+ margin-bottom: 15px;
1309
+ padding-top: 10px;
1310
+ border-top: 1px dashed #ccc;
1311
+ }
1312
+ .product-photos-preview img {
1313
+ max-width: 80px;
1314
+ height: 80px;
1315
+ object-fit: cover;
1316
+ border-radius: 5px;
1317
+ border: 1px solid #ddd;
1318
+ }
1319
  </style>
1320
  </head>
1321
  <body>
 
1324
  <img src="''' + LOGO_URL + '''" alt="Logo" class="header-logo">
1325
  <h1>Админ-панель</h1>
1326
  </div>
1327
+
1328
+ <h1>Добавление нового товара</h1>
1329
  <form method="POST" enctype="multipart/form-data">
1330
  <input type="hidden" name="action" value="add">
1331
+ <label for="add-name">Название товара:</label>
1332
+ <input type="text" id="add-name" name="name" required>
1333
+
1334
+ <label for="add-price">Цена ($):</label>
1335
+ <input type="number" id="add-price" name="price" step="0.01" required>
1336
+
1337
+ <label for="add-description">Описание:</label>
1338
+ <textarea id="add-description" name="description" rows="4" required></textarea>
1339
+
1340
+ <label for="add-category">Категория:</label>
1341
+ <select id="add-category" name="category">
1342
  <option value="Без категории">Без категории</option>
1343
  {% for category in categories %}
1344
  <option value="{{ category }}">{{ category }}</option>
1345
  {% endfor %}
1346
  </select>
1347
+
1348
+ <label for="add-photos">Фотографии (до 10):</label>
1349
+ <input type="file" id="add-photos" name="photos" accept="image/*" multiple>
1350
+
1351
  <label>Цвета:</label>
1352
  <div id="color-inputs-add">
1353
  <div class="color-input-group">
1354
  <input type="text" name="colors" placeholder="Например: Красный">
1355
+ <button type="button" class="remove-color-btn" onclick="this.parentElement.remove()">Удалить</button>
1356
  </div>
1357
  </div>
1358
  <button type="button" class="add-color-btn" onclick="addColorInput('color-inputs-add')">Добавить цвет</button>
1359
+
1360
  <button type="submit">Добавить товар</button>
1361
  </form>
1362
 
1363
+ <h2>Управление категориями</h2>
1364
  <form method="POST">
1365
  <input type="hidden" name="action" value="add_category">
1366
+ <label for="add-category-name">Название категории:</label>
1367
+ <input type="text" id="add-category-name" name="category_name" required>
1368
+ <button type="submit">Добавить категорию</button>
1369
  </form>
1370
 
 
1371
  <div class="category-list">
1372
  {% for category in categories %}
1373
  <div class="category-item">
1374
+ <span>{{ category }}</span>
1375
+ <form method="POST">
1376
  <input type="hidden" name="action" value="delete_category">
1377
  <input type="hidden" name="category_index" value="{{ loop.index0 }}">
1378
  <button type="submit" class="delete-button">Удалить</button>
 
1381
  {% endfor %}
1382
  </div>
1383
 
1384
+ <h2>Управление базой данных</h2>
1385
+ <form method="POST" action="{{ url_for('backup') }}" style="display: inline-block; margin-right: 10px; background: none; padding: 0; box-shadow: none;">
1386
+ <button type="submit">Создать резервную копию на HF</button>
1387
+ </form>
1388
+ <form method="GET" action="{{ url_for('download') }}" style="display: inline-block; background: none; padding: 0; box-shadow: none;">
1389
+ <button type="submit">Скачать базу с HF</button>
1390
+ </form>
1391
+
1392
 
1393
  <h2>Список товаров</h2>
1394
  <div class="product-list">
 
1396
  <div class="product-item">
1397
  <h3>{{ product['name'] }}</h3>
1398
  <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
1399
+ <p><strong>Цена:</strong> {{ "{:.2f}".format(product['price']) }} $</p>
1400
+ <p><strong>Описание:</strong> {{ product['description'][:150] }}{% if product['description']|length > 150 %}...{% endif %}</p>
1401
+ <p><strong>Цвета:</strong> {{ product.get('colors', ['Без цветов'])|join(', ') }}</p>
1402
  {% if product.get('photos') and product['photos']|length > 0 %}
1403
+ <div class="product-photos-preview">
1404
  {% for photo in product['photos'] %}
1405
  <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo }}"
1406
+ alt="{{ product['name'] }}">
 
1407
  {% endfor %}
1408
  </div>
1409
  {% endif %}
 
1414
  <input type="hidden" name="index" value="{{ loop.index0 }}">
1415
  <label>Название:</label>
1416
  <input type="text" name="name" value="{{ product['name'] }}" required>
1417
+ <label>Цена ($):</label>
1418
  <input type="number" name="price" step="0.01" value="{{ product['price'] }}" required>
1419
  <label>Описание:</label>
1420
  <textarea name="description" rows="4" required>{{ product['description'] }}</textarea>
 
1435
  <button type="button" class="remove-color-btn" onclick="this.parentElement.remove()">Удалить</button>
1436
  </div>
1437
  {% endfor %}
1438
+ {% if not product.get('colors') or product.get('colors')|length == 0 %}
1439
  <div class="color-input-group">
1440
  <input type="text" name="colors" placeholder="Например: Синий">
1441
  <button type="button" class="remove-color-btn" onclick="this.parentElement.remove()">Удалить</button>
 
1443
  {% endif %}
1444
  </div>
1445
  <button type="button" class="add-color-btn" onclick="addColorInput('edit-color-inputs-{{ loop.index0 }}')">Добавить цвет</button>
1446
+ <button type="submit">Сохранить изменения</button>
1447
  </form>
1448
  </details>
1449
+ <form method="POST" style="display: inline-block; margin-top: 15px;">
1450
  <input type="hidden" name="action" value="delete">
1451
  <input type="hidden" name="index" value="{{ loop.index0 }}">
1452
+ <button type="submit" class="delete-button">Удалить товар</button>
1453
  </form>
1454
  </div>
1455
  {% endfor %}
 
1460
  const container = document.getElementById(containerId);
1461
  const newInputGroup = document.createElement('div');
1462
  newInputGroup.className = 'color-input-group';
1463
+
1464
  const newInput = document.createElement('input');
1465
  newInput.type = 'text';
1466
  newInput.name = 'colors';
1467
  newInput.placeholder = 'Например: Красный';
1468
+
1469
  const removeBtn = document.createElement('button');
1470
  removeBtn.type = 'button';
1471
  removeBtn.className = 'remove-color-btn';
 
1473
  removeBtn.onclick = function() {
1474
  newInputGroup.remove();
1475
  };
1476
+
1477
  newInputGroup.appendChild(newInput);
1478
  newInputGroup.appendChild(removeBtn);
1479
  container.appendChild(newInputGroup);
1480
  }
1481
+
1482
+ // Ensure at least one color input exists when loading edit form
1483
+ document.querySelectorAll('details summary').forEach(summary => {
1484
+ summary.addEventListener('click', () => {
1485
+ // Use a small timeout to allow the details element to open
1486
+ setTimeout(() => {
1487
+ const detailContent = summary.nextElementSibling;
1488
+ if (detailContent && detailContent.classList.contains('edit-form')) {
1489
+ const colorContainer = detailContent.querySelector('[id^="edit-color-inputs-"]');
1490
+ if (colorContainer && colorContainer.children.length === 0) {
1491
+ addColorInput(colorContainer.id);
1492
+ }
1493
+ }
1494
+ }, 50);
1495
+ });
1496
+ });
1497
  </script>
1498
  </body>
1499
  </html>
 
1502
 
1503
  @app.route('/backup', methods=['POST'])
1504
  def backup():
1505
+ if not HF_TOKEN_WRITE:
1506
+ return "Ошибка: HF_TOKEN_WRITE не установлен.", 500
1507
+ try:
1508
+ upload_db_to_hf()
1509
+ return "Резервная копия создана.", 200
1510
+ except Exception as e:
1511
+ logging.error(f"Ошибка при ручном резервном копировании: {e}")
1512
+ return f"Ошибка при создании резервной копии: {e}", 500
1513
 
1514
  @app.route('/download', methods=['GET'])
1515
  def download():
1516
+ if not HF_TOKEN_READ:
1517
+ return "Ошибка: HF_TOKEN_READ не установлен.", 500
1518
+ try:
1519
+ download_db_from_hf()
1520
+ return "База данных скачана.", 200
1521
+ except Exception as e:
1522
+ logging.error(f"Ошибка при ручном скачивании базы: {e}")
1523
+ return f"Ошибка при скачивании базы данных: {e}", 500
1524
 
1525
  if __name__ == '__main__':
1526
  if HF_TOKEN_WRITE and REPO_ID:
 
1529
  try:
1530
  load_data()
1531
  except Exception as e:
1532
+ logging.error(f"Не удалось загрузить базу данных при запуске: {e}")
1533
  app.run(debug=True, host='0.0.0.0', port=7860)