flpolprojects commited on
Commit
a9bbef6
·
verified ·
1 Parent(s): 99c03dd

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +178 -282
app.py CHANGED
@@ -1,4 +1,4 @@
1
- from flask import Flask, render_template_string, request, redirect, url_for
2
  import json
3
  import os
4
  import logging
@@ -12,12 +12,12 @@ from werkzeug.utils import secure_filename
12
  app = Flask(__name__)
13
  DATA_FILE = 'products.json'
14
 
15
- # Настройки Hugging Face
16
  REPO_ID = "flpolprojects/Clients"
17
  HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
18
  HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
19
 
20
- # Настройка логирования
21
  logging.basicConfig(level=logging.DEBUG)
22
 
23
  def load_data():
@@ -26,16 +26,16 @@ def load_data():
26
  with open(DATA_FILE, 'r', encoding='utf-8') as file:
27
  return json.load(file)
28
  except FileNotFoundError:
29
- logging.warning("Локальный файл базы данных не найден после скачивания.")
30
  return []
31
  except json.JSONDecodeError:
32
- logging.error("Ошибка: Невозможно декодировать JSON файл.")
33
  return []
34
  except RepositoryNotFoundError:
35
- logging.error("Репозиторий не найден. Создание локальной базы данных.")
36
  return []
37
  except Exception as e:
38
- logging.error(f"Произошла ошибка при загрузке данных: {e}")
39
  return []
40
 
41
  def save_data(data):
@@ -43,7 +43,7 @@ def save_data(data):
43
  with open(DATA_FILE, 'w', encoding='utf-8') as file:
44
  json.dump(data, file, ensure_ascii=False, indent=4)
45
  except Exception as e:
46
- logging.error(f"Ошибка при сохранении данных: {e}")
47
  raise
48
 
49
  def upload_db_to_hf():
@@ -52,13 +52,14 @@ def upload_db_to_hf():
52
  api.upload_file(
53
  path_or_fileobj=DATA_FILE,
54
  path_in_repo=DATA_FILE,
 
55
  repo_type="dataset",
56
  token=HF_TOKEN_WRITE,
57
- commit_message=f"Автоматическое резервное копирование базы данных {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
58
  )
59
- logging.info("Резервная копия JSON базы успешно загружена на Hugging Face.")
60
  except Exception as e:
61
- logging.error(f"Ошибка при загрузке резервной копии: {e}")
62
 
63
  def download_db_from_hf():
64
  try:
@@ -70,18 +71,18 @@ def download_db_from_hf():
70
  local_dir=".",
71
  local_dir_use_symlinks=False
72
  )
73
- logging.info("JSON база успешно скачана из Hugging Face.")
74
  except RepositoryNotFoundError as e:
75
- logging.error(f"Репозиторий не найден: {e}")
76
  raise
77
  except Exception as e:
78
- logging.error(f"Ошибка при скачивании JSON базы: {e}")
79
  raise
80
 
81
  def periodic_backup():
82
  while True:
83
  upload_db_to_hf()
84
- time.sleep(15)
85
 
86
  @app.route('/')
87
  def catalog():
@@ -92,9 +93,9 @@ def catalog():
92
  <head>
93
  <meta charset="UTF-8">
94
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
95
- <title>Каталог</title>
96
- <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
97
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.css">
98
  <style>
99
  * {
100
  margin: 0;
@@ -102,15 +103,15 @@ def catalog():
102
  box-sizing: border-box;
103
  }
104
  body {
105
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
106
- background-color: #f5f5f5;
107
  color: #333;
108
  line-height: 1.6;
109
- padding: 20px;
110
  }
111
  .container {
112
  max-width: 1200px;
113
  margin: 0 auto;
 
114
  }
115
  h1 {
116
  text-align: center;
@@ -121,29 +122,24 @@ def catalog():
121
  }
122
  .products-grid {
123
  display: grid;
124
- grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
125
- gap: 20px;
126
- padding: 0 15px;
127
  }
128
  .product {
129
  background: #ffffff;
130
  border-radius: 12px;
131
- padding: 20px;
132
  transition: transform 0.3s ease, box-shadow 0.3s ease;
133
- display: flex;
134
- flex-direction: column;
135
- box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1);
136
  }
137
  .product:hover {
138
  transform: translateY(-5px);
139
- box-shadow: 0 5px 25px rgba(0, 0, 0, 0.15);
140
  }
141
  .product-image {
142
  width: 100%;
143
  height: 200px;
144
  overflow: hidden;
145
- border-radius: 8px;
146
- margin-bottom: 15px;
147
  }
148
  .product-image img {
149
  width: 100%;
@@ -154,11 +150,14 @@ def catalog():
154
  .product-image img:hover {
155
  transform: scale(1.05);
156
  }
 
 
 
157
  .product h2 {
158
  font-size: 1.2em;
159
  color: #2c3e50;
160
  margin-bottom: 10px;
161
- font-weight: 600;
162
  }
163
  .product-price {
164
  font-size: 1.3em;
@@ -169,10 +168,10 @@ def catalog():
169
  .product-description {
170
  color: #7f8c8d;
171
  font-size: 0.9em;
172
- flex-grow: 1;
173
  margin-bottom: 15px;
174
  }
175
  .product-button {
 
176
  background-color: #3498db;
177
  color: white;
178
  padding: 10px 20px;
@@ -180,152 +179,63 @@ def catalog():
180
  border-radius: 5px;
181
  cursor: pointer;
182
  transition: background-color 0.3s ease;
183
- text-align: center;
184
  text-decoration: none;
185
- display: inline-block;
186
  font-weight: 500;
187
  }
188
  .product-button:hover {
189
  background-color: #2980b9;
190
  }
191
- @media (max-width: 768px) {
192
- .products-grid {
193
- grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
194
- gap: 10px;
195
- }
196
- body {
197
- padding: 10px;
198
- }
199
- h1 {
200
- font-size: 1.8em;
201
- margin-bottom: 20px;
202
- }
203
- .product {
204
- padding: 10px;
205
- }
206
- .product-image {
207
- height: 120px;
208
- }
209
- .product h2 {
210
- font-size: 1em;
211
- }
212
- .product-price {
213
- font-size: 1.1em;
214
- }
215
- .product-description {
216
- font-size: 0.8em;
217
- }
218
- .product-button {
219
- padding: 8px 15px;
220
- font-size: 0.9em;
221
- }
222
- }
223
- @keyframes fadeIn {
224
- from {
225
- opacity: 0;
226
- transform: translateY(20px);
227
- }
228
- to {
229
- opacity: 1;
230
- transform: translateY(0);
231
- }
232
- }
233
- .product {
234
- animation: fadeIn 0.5s ease-out forwards;
235
- }
236
- ::-webkit-scrollbar {
237
- width: 8px;
238
- }
239
- ::-webkit-scrollbar-track {
240
- background: #f1f1f1;
241
- }
242
- ::-webkit-scrollbar-thumb {
243
- background: #888;
244
- border-radius: 4px;
245
- }
246
- ::-webkit-scrollbar-thumb:hover {
247
- background: #555;
248
- }
249
-
250
- /* Modal Styles */
251
  .modal {
252
- display: none; /* Hidden by default */
253
- position: fixed; /* Stay in place */
254
- z-index: 1; /* Sit on top */
255
  left: 0;
256
  top: 0;
257
- width: 100%; /* Full width */
258
- height: 100%; /* Full height */
259
- overflow: auto; /* Enable scroll if needed */
260
- background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
261
  }
262
-
263
  .modal-content {
264
- position: relative;
265
  background-color: #fefefe;
266
- margin: 10% auto; /* 15% from the top and centered */
267
  padding: 20px;
268
  border: 1px solid #888;
269
  width: 80%;
270
  max-width: 600px;
271
- box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19);
272
- animation-name: animatetop;
273
- animation-duration: 0.4s
274
  }
275
-
276
- /* Add Animation */
277
- @keyframes animatetop {
278
- from {top: -300px; opacity: 0}
279
- to {top: 10%; opacity: 1}
280
- }
281
-
282
- /* The Close Button */
283
  .close {
284
  color: #aaa;
285
  float: right;
286
  font-size: 28px;
287
  font-weight: bold;
 
288
  }
289
-
290
  .close:hover,
291
  .close:focus {
292
- color: black;
293
  text-decoration: none;
294
- cursor: pointer;
295
  }
296
-
297
- /* Swiper Styles */
298
  .swiper-container {
299
  width: 100%;
300
  height: 300px;
301
  }
302
-
303
- .swiper-slide {
304
- text-align: center;
305
- font-size: 18px;
306
- background: #fff;
307
-
308
- /* Center slide text vertically */
309
- display: -webkit-box;
310
- display: -ms-flexbox;
311
- display: -webkit-flex;
312
- display: flex;
313
- -webkit-box-pack: center;
314
- -ms-flex-pack: center;
315
- -webkit-justify-content: center;
316
- justify-content: center;
317
- -webkit-box-align: center;
318
- -ms-flex-align: center;
319
- -webkit-align-items: center;
320
- align-items: center;
321
- }
322
-
323
  .swiper-slide img {
324
- display: block;
325
  width: 100%;
326
  height: 100%;
327
  object-fit: cover;
328
  }
 
 
 
 
 
 
 
 
 
329
  </style>
330
  </head>
331
  <body>
@@ -334,68 +244,54 @@ def catalog():
334
  <div class="products-grid">
335
  {% for product in products %}
336
  <div class="product">
337
- {% if product.get('photos') and product['photos']|length > 0 %}
338
  <div class="product-image">
 
339
  <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product['photos'][0] }}"
340
  alt="{{ product['name'] }}"
341
  loading="lazy">
 
 
 
 
 
 
 
 
 
342
  </div>
343
- {% endif %}
344
- <h2>{{ product['name'] }}</h2>
345
- <div class="product-price">{{ product['price'] }} ₽</div>
346
- <p class="product-description">{{ product['description'][:100] }}{% if product['description']|length > 100 %}...{% endif %}</p>
347
- <button class="product-button" onclick="openModal({{ loop.index0 }})">Подробнее</button>
348
  </div>
349
  {% endfor %}
350
  </div>
351
  </div>
352
 
353
- <!-- The Modal -->
354
  <div id="productModal" class="modal">
355
  <div class="modal-content">
356
  <span class="close" onclick="closeModal()">&times;</span>
357
- <div id="modalContent">
358
- <!-- Product details will be loaded here -->
359
- </div>
360
  </div>
361
  </div>
362
 
363
- <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
364
- <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.5.3/dist/umd/popper.min.js"></script>
365
- <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
366
- <script src="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.js"></script>
367
  <script>
368
- // Function to open the modal
369
  function openModal(index) {
370
- loadProductDetails(index);
371
- document.getElementById('productModal').style.display = "block";
372
- }
373
-
374
- // Function to close the modal
375
- function closeModal() {
376
- document.getElementById('productModal').style.display = "none";
377
- }
378
-
379
- // Function to load product details into the modal
380
- function loadProductDetails(index) {
381
  fetch('/product/' + index)
382
  .then(response => response.text())
383
  .then(data => {
384
  document.getElementById('modalContent').innerHTML = data;
385
- // Initialize Swiper after the content is loaded
386
- initializeSwiper();
387
  });
388
  }
389
 
390
- function initializeSwiper() {
391
- var swiper = new Swiper('.swiper-container', {
392
- slidesPerView: 1,
393
- spaceBetween: 30,
 
 
394
  loop: true,
395
- grabCursor: true, // Enables the ability to grab with a cursor
396
  pagination: {
397
  el: '.swiper-pagination',
398
- clickable: true,
399
  },
400
  navigation: {
401
  nextEl: '.swiper-button-next',
@@ -404,7 +300,6 @@ def catalog():
404
  });
405
  }
406
 
407
- // Close the modal if the user clicks outside of it
408
  window.onclick = function(event) {
409
  if (event.target == document.getElementById('productModal')) {
410
  closeModal();
@@ -422,9 +317,9 @@ def product_detail(index):
422
  try:
423
  product = products[index]
424
  except IndexError:
425
- return "Продукт не найден", 404
426
  detail_html = '''
427
- <div class="container">
428
  <h2>{{ product['name'] }}</h2>
429
  <div class="swiper-container">
430
  <div class="swiper-wrapper">
@@ -440,14 +335,12 @@ def product_detail(index):
440
  </div>
441
  {% endif %}
442
  </div>
443
- <!-- Add Pagination -->
444
  <div class="swiper-pagination"></div>
445
- <!-- Add Navigation -->
446
  <div class="swiper-button-next"></div>
447
  <div class="swiper-button-prev"></div>
448
  </div>
449
- <p><strong>Цена:</strong> {{ product['price'] }} ₽</p>
450
- <p><strong>Описание:</strong> {{ product['description'] }}</p>
451
  </div>
452
  '''
453
  return render_template_string(detail_html, product=product, repo_id=REPO_ID)
@@ -464,7 +357,7 @@ def admin():
464
  photos_files = request.files.getlist('photos')
465
  photos_list = []
466
  if photos_files:
467
- for photo in photos_files:
468
  if photo and photo.filename:
469
  photo_filename = secure_filename(photo.filename)
470
  uploads_dir = 'uploads'
@@ -479,19 +372,19 @@ def admin():
479
  repo_id=REPO_ID,
480
  repo_type="dataset",
481
  token=HF_TOKEN_WRITE,
482
- commit_message=f"Добавлено фото для товара {name}"
483
  )
484
  photos_list.append(photo_filename)
485
  except Exception as e:
486
- logging.error(f"Ошибка при загрузке фото: {e}")
487
- return f"Ошибка при загрузке фото: {e}", 500
488
  finally:
489
  os.remove(temp_path)
490
  if name and price and description:
491
  try:
492
  price = float(price.replace(',', '.'))
493
  except ValueError:
494
- return "Ошибка: Цена должна быть числом.", 400
495
  product = {
496
  'name': name,
497
  'price': price,
@@ -507,10 +400,9 @@ def admin():
507
  price = request.form.get('price')
508
  description = request.form.get('description')
509
  photos_files = request.files.getlist('photos')
510
- # Если загружены новые фото, обновляем список фотографий товара
511
  if photos_files and any(photo.filename for photo in photos_files):
512
  new_photos_list = []
513
- for photo in photos_files:
514
  if photo and photo.filename:
515
  photo_filename = secure_filename(photo.filename)
516
  uploads_dir = 'uploads'
@@ -525,12 +417,12 @@ def admin():
525
  repo_id=REPO_ID,
526
  repo_type="dataset",
527
  token=HF_TOKEN_WRITE,
528
- commit_message=f"Обновлено фото для товара {name}"
529
  )
530
  new_photos_list.append(photo_filename)
531
  except Exception as e:
532
- logging.error(f"Ошибка при загрузке фото: {e}")
533
- return f"Ошибка при загрузке фото: {e}", 500
534
  finally:
535
  os.remove(temp_path)
536
  products[index]['photos'] = new_photos_list
@@ -538,7 +430,7 @@ def admin():
538
  try:
539
  price = float(price.replace(',', '.'))
540
  except ValueError:
541
- return "Ошибка: Цена должна быть числом.", 400
542
  products[index]['price'] = price
543
  products[index]['description'] = description
544
  save_data(products)
@@ -555,21 +447,27 @@ def admin():
555
  <meta charset="UTF-8">
556
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
557
  <title>Админ-панель</title>
 
558
  <style>
559
  body {
560
- font-family: Arial, sans-serif;
561
- margin: 20px;
562
- background-color: #f9f9f9;
563
- }
564
- h1 {
565
  color: #333;
566
  }
 
 
 
 
 
 
 
567
  form {
568
  background-color: #fff;
569
  padding: 20px;
570
- border: 1px solid #ddd;
571
- border-radius: 5px;
572
- max-width: 100%;
573
  margin-bottom: 20px;
574
  }
575
  label {
@@ -588,112 +486,110 @@ def admin():
588
  button {
589
  margin-top: 15px;
590
  padding: 10px 15px;
591
- background-color: #28a745;
592
  color: white;
593
  border: none;
594
  border-radius: 4px;
595
  cursor: pointer;
 
596
  }
597
  button:hover {
598
- background-color: #218838;
599
  }
600
  .product-list {
601
- margin-top: 20px;
 
 
602
  }
603
  .product-item {
604
  background-color: #fff;
605
  border: 1px solid #ddd;
606
  padding: 15px;
607
- margin-bottom: 10px;
608
- border-radius: 5px;
 
 
 
 
 
609
  }
610
  .edit-form {
611
  margin-top: 10px;
612
  padding: 10px;
613
  border: 1px solid #ddd;
614
- border-radius: 5px;
615
  background-color: #f9f9f9;
616
  }
617
  @media (max-width: 600px) {
618
- body {
619
- margin: 10px;
620
- }
621
- h1 {
622
- font-size: 24px;
623
- }
624
- form {
625
- padding: 10px;
626
- }
627
- .product-item {
628
- padding: 10px;
629
- }
630
- input[type="file"] {
631
- margin-bottom: 10px;
632
  }
633
  }
634
  </style>
635
  </head>
636
  <body>
637
- <h1>Добавление товара</h1>
638
- <form method="POST" enctype="multipart/form-data">
639
- <input type="hidden" name="action" value="add">
640
- <label for="name">Название товара:</label>
641
- <input type="text" id="name" name="name" required>
642
- <label for="price">Цена:</label>
643
- <input type="number" id="price" name="price" step="0.01" required>
644
- <label for="description">Описание:</label>
645
- <textarea id="description" name="description" rows="4" required></textarea>
646
- <label for="photos">Фотографии товара (до 5):</label>
647
- <input type="file" id="photos" name="photos" accept="image/*" multiple>
648
- <button type="submit">Добавить товар</button>
649
- </form>
650
-
651
- <h2>Управление базой данных</h2>
652
- <form method="POST" action="{{ url_for('backup') }}">
653
- <button type="submit">Создать резервную копию</button>
654
- </form>
655
-
656
- <form method="GET" action="{{ url_for('download') }}">
657
- <button type="submit">Скачать базу данных</button>
658
- </form>
659
-
660
- <h2>Список товаров</h2>
661
- <div class="product-list">
662
- {% for product in products %}
663
- <div class="product-item">
664
- <h3>{{ product['name'] }}</h3>
665
- <p><strong>Цена:</strong> {{ product['price'] }} руб.</p>
666
- <p><strong>Описание:</strong> {{ product['description'] }}</p>
667
- {% if product.get('photos') and product['photos']|length > 0 %}
668
- <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product['photos'][0] }}"
669
- alt="{{ product['name'] }}"
670
- style="max-width: 100px;">
671
- {% endif %}
 
672
 
673
- <details>
674
- <summary>Редактировать</summary>
675
- <form method="POST" enctype="multipart/form-data" class="edit-form">
676
- <input type="hidden" name="action" value="edit">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
677
  <input type="hidden" name="index" value="{{ loop.index0 }}">
678
- <label for="name">Название товара:</label>
679
- <input type="text" id="name" name="name" value="{{ product['name'] }}" required>
680
- <label for="price">Цена:</label>
681
- <input type="number" id="price" name="price" step="0.01" value="{{ product['price'] }}" required>
682
- <label for="description">Описание:</label>
683
- <textarea id="description" name="description" rows="4" required>{{ product['description'] }}</textarea>
684
- <label for="photos">Фотографии товара (до 5):</label>
685
- <input type="file" id="photos" name="photos" accept="image/*" multiple>
686
- <button type="submit">Сохранить изменения</button>
687
  </form>
688
- </details>
689
-
690
- <form method="POST">
691
- <input type="hidden" name="action" value="delete">
692
- <input type="hidden" name="index" value="{{ loop.index0 }}">
693
- <button type="submit">Удалить</button>
694
- </form>
695
  </div>
696
- {% endfor %}
697
  </div>
698
  </body>
699
  </html>
@@ -708,7 +604,7 @@ def backup():
708
  @app.route('/download', methods=['GET'])
709
  def download():
710
  download_db_from_hf()
711
- return "База данных успешно скачана.", 200
712
 
713
  if __name__ == '__main__':
714
  backup_thread = threading.Thread(target=periodic_backup, daemon=True)
@@ -717,6 +613,6 @@ if __name__ == '__main__':
717
  try:
718
  load_data()
719
  except Exception as e:
720
- logging.error(f"Не удалось загрузить базу данных при запуске: {e}")
721
 
722
- app.run(debug=True, host='0.0.0.0', port=7860)
 
1
+ from flask import Flask, render_template_string, request, redirect, url_for, send_file
2
  import json
3
  import os
4
  import logging
 
12
  app = Flask(__name__)
13
  DATA_FILE = 'products.json'
14
 
15
+ # Hugging Face settings
16
  REPO_ID = "flpolprojects/Clients"
17
  HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
18
  HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
19
 
20
+ # Logging configuration
21
  logging.basicConfig(level=logging.DEBUG)
22
 
23
  def load_data():
 
26
  with open(DATA_FILE, 'r', encoding='utf-8') as file:
27
  return json.load(file)
28
  except FileNotFoundError:
29
+ logging.warning("Local database file not found after download.")
30
  return []
31
  except json.JSONDecodeError:
32
+ logging.error("Error: Unable to decode JSON file.")
33
  return []
34
  except RepositoryNotFoundError:
35
+ logging.error("Repository not found. Creating local database.")
36
  return []
37
  except Exception as e:
38
+ logging.error(f"An error occurred while loading data: {e}")
39
  return []
40
 
41
  def save_data(data):
 
43
  with open(DATA_FILE, 'w', encoding='utf-8') as file:
44
  json.dump(data, file, ensure_ascii=False, indent=4)
45
  except Exception as e:
46
+ logging.error(f"Error saving data: {e}")
47
  raise
48
 
49
  def upload_db_to_hf():
 
52
  api.upload_file(
53
  path_or_fileobj=DATA_FILE,
54
  path_in_repo=DATA_FILE,
55
+ repo_id=REPO_ID,
56
  repo_type="dataset",
57
  token=HF_TOKEN_WRITE,
58
+ commit_message=f"Automatic database backup {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
59
  )
60
+ logging.info("JSON database backup successfully uploaded to Hugging Face.")
61
  except Exception as e:
62
+ logging.error(f"Error uploading backup: {e}")
63
 
64
  def download_db_from_hf():
65
  try:
 
71
  local_dir=".",
72
  local_dir_use_symlinks=False
73
  )
74
+ logging.info("JSON database successfully downloaded from Hugging Face.")
75
  except RepositoryNotFoundError as e:
76
+ logging.error(f"Repository not found: {e}")
77
  raise
78
  except Exception as e:
79
+ logging.error(f"Error downloading JSON database: {e}")
80
  raise
81
 
82
  def periodic_backup():
83
  while True:
84
  upload_db_to_hf()
85
+ time.sleep(900) # 15 minutes
86
 
87
  @app.route('/')
88
  def catalog():
 
93
  <head>
94
  <meta charset="UTF-8">
95
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
96
+ <title>Каталог товаров</title>
97
+ <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
98
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/8.4.5/swiper-bundle.min.css">
99
  <style>
100
  * {
101
  margin: 0;
 
103
  box-sizing: border-box;
104
  }
105
  body {
106
+ font-family: 'Roboto', sans-serif;
107
+ background-color: #f0f2f5;
108
  color: #333;
109
  line-height: 1.6;
 
110
  }
111
  .container {
112
  max-width: 1200px;
113
  margin: 0 auto;
114
+ padding: 20px;
115
  }
116
  h1 {
117
  text-align: center;
 
122
  }
123
  .products-grid {
124
  display: grid;
125
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
126
+ gap: 30px;
 
127
  }
128
  .product {
129
  background: #ffffff;
130
  border-radius: 12px;
131
+ overflow: hidden;
132
  transition: transform 0.3s ease, box-shadow 0.3s ease;
133
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
 
 
134
  }
135
  .product:hover {
136
  transform: translateY(-5px);
137
+ box-shadow: 0 10px 20px rgba(0, 0, 0, 0.15);
138
  }
139
  .product-image {
140
  width: 100%;
141
  height: 200px;
142
  overflow: hidden;
 
 
143
  }
144
  .product-image img {
145
  width: 100%;
 
150
  .product-image img:hover {
151
  transform: scale(1.05);
152
  }
153
+ .product-info {
154
+ padding: 20px;
155
+ }
156
  .product h2 {
157
  font-size: 1.2em;
158
  color: #2c3e50;
159
  margin-bottom: 10px;
160
+ font-weight: 500;
161
  }
162
  .product-price {
163
  font-size: 1.3em;
 
168
  .product-description {
169
  color: #7f8c8d;
170
  font-size: 0.9em;
 
171
  margin-bottom: 15px;
172
  }
173
  .product-button {
174
+ display: inline-block;
175
  background-color: #3498db;
176
  color: white;
177
  padding: 10px 20px;
 
179
  border-radius: 5px;
180
  cursor: pointer;
181
  transition: background-color 0.3s ease;
 
182
  text-decoration: none;
 
183
  font-weight: 500;
184
  }
185
  .product-button:hover {
186
  background-color: #2980b9;
187
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  .modal {
189
+ display: none;
190
+ position: fixed;
191
+ z-index: 1000;
192
  left: 0;
193
  top: 0;
194
+ width: 100%;
195
+ height: 100%;
196
+ overflow: auto;
197
+ background-color: rgba(0,0,0,0.4);
198
  }
 
199
  .modal-content {
 
200
  background-color: #fefefe;
201
+ margin: 10% auto;
202
  padding: 20px;
203
  border: 1px solid #888;
204
  width: 80%;
205
  max-width: 600px;
206
+ border-radius: 10px;
207
+ box-shadow: 0 4px 8px rgba(0,0,0,0.1);
 
208
  }
 
 
 
 
 
 
 
 
209
  .close {
210
  color: #aaa;
211
  float: right;
212
  font-size: 28px;
213
  font-weight: bold;
214
+ cursor: pointer;
215
  }
 
216
  .close:hover,
217
  .close:focus {
218
+ color: #000;
219
  text-decoration: none;
 
220
  }
 
 
221
  .swiper-container {
222
  width: 100%;
223
  height: 300px;
224
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
  .swiper-slide img {
 
226
  width: 100%;
227
  height: 100%;
228
  object-fit: cover;
229
  }
230
+ @media (max-width: 768px) {
231
+ .products-grid {
232
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
233
+ gap: 20px;
234
+ }
235
+ .product-image {
236
+ height: 150px;
237
+ }
238
+ }
239
  </style>
240
  </head>
241
  <body>
 
244
  <div class="products-grid">
245
  {% for product in products %}
246
  <div class="product">
 
247
  <div class="product-image">
248
+ {% if product.get('photos') and product['photos']|length > 0 %}
249
  <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product['photos'][0] }}"
250
  alt="{{ product['name'] }}"
251
  loading="lazy">
252
+ {% else %}
253
+ <img src="https://via.placeholder.com/300" alt="No Image">
254
+ {% endif %}
255
+ </div>
256
+ <div class="product-info">
257
+ <h2>{{ product['name'] }}</h2>
258
+ <div class="product-price">{{ product['price'] }} ₽</div>
259
+ <p class="product-description">{{ product['description'][:100] }}{% if product['description']|length > 100 %}...{% endif %}</p>
260
+ <button class="product-button" onclick="openModal({{ loop.index0 }})">Подробнее</button>
261
  </div>
 
 
 
 
 
262
  </div>
263
  {% endfor %}
264
  </div>
265
  </div>
266
 
 
267
  <div id="productModal" class="modal">
268
  <div class="modal-content">
269
  <span class="close" onclick="closeModal()">&times;</span>
270
+ <div id="modalContent"></div>
 
 
271
  </div>
272
  </div>
273
 
274
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/Swiper/8.4.5/swiper-bundle.min.js"></script>
 
 
 
275
  <script>
 
276
  function openModal(index) {
 
 
 
 
 
 
 
 
 
 
 
277
  fetch('/product/' + index)
278
  .then(response => response.text())
279
  .then(data => {
280
  document.getElementById('modalContent').innerHTML = data;
281
+ document.getElementById('productModal').style.display = "block";
282
+ initSwiper();
283
  });
284
  }
285
 
286
+ function closeModal() {
287
+ document.getElementById('productModal').style.display = "none";
288
+ }
289
+
290
+ function initSwiper() {
291
+ new Swiper('.swiper-container', {
292
  loop: true,
 
293
  pagination: {
294
  el: '.swiper-pagination',
 
295
  },
296
  navigation: {
297
  nextEl: '.swiper-button-next',
 
300
  });
301
  }
302
 
 
303
  window.onclick = function(event) {
304
  if (event.target == document.getElementById('productModal')) {
305
  closeModal();
 
317
  try:
318
  product = products[index]
319
  except IndexError:
320
+ return "Product not found", 404
321
  detail_html = '''
322
+ <div class="product-detail">
323
  <h2>{{ product['name'] }}</h2>
324
  <div class="swiper-container">
325
  <div class="swiper-wrapper">
 
335
  </div>
336
  {% endif %}
337
  </div>
 
338
  <div class="swiper-pagination"></div>
 
339
  <div class="swiper-button-next"></div>
340
  <div class="swiper-button-prev"></div>
341
  </div>
342
+ <p class="product-price">{{ product['price'] }} ₽</p>
343
+ <p class="product-description">{{ product['description'] }}</p>
344
  </div>
345
  '''
346
  return render_template_string(detail_html, product=product, repo_id=REPO_ID)
 
357
  photos_files = request.files.getlist('photos')
358
  photos_list = []
359
  if photos_files:
360
+ for photo in photos_files[:2]: # Limit to 2 photos
361
  if photo and photo.filename:
362
  photo_filename = secure_filename(photo.filename)
363
  uploads_dir = 'uploads'
 
372
  repo_id=REPO_ID,
373
  repo_type="dataset",
374
  token=HF_TOKEN_WRITE,
375
+ commit_message=f"Added photo for product {name}"
376
  )
377
  photos_list.append(photo_filename)
378
  except Exception as e:
379
+ logging.error(f"Error uploading photo: {e}")
380
+ return f"Error uploading photo: {e}", 500
381
  finally:
382
  os.remove(temp_path)
383
  if name and price and description:
384
  try:
385
  price = float(price.replace(',', '.'))
386
  except ValueError:
387
+ return "Error: Price must be a number.", 400
388
  product = {
389
  'name': name,
390
  'price': price,
 
400
  price = request.form.get('price')
401
  description = request.form.get('description')
402
  photos_files = request.files.getlist('photos')
 
403
  if photos_files and any(photo.filename for photo in photos_files):
404
  new_photos_list = []
405
+ for photo in photos_files[:2]: # Limit to 2 photos
406
  if photo and photo.filename:
407
  photo_filename = secure_filename(photo.filename)
408
  uploads_dir = 'uploads'
 
417
  repo_id=REPO_ID,
418
  repo_type="dataset",
419
  token=HF_TOKEN_WRITE,
420
+ commit_message=f"Updated photo for product {name}"
421
  )
422
  new_photos_list.append(photo_filename)
423
  except Exception as e:
424
+ logging.error(f"Error uploading photo: {e}")
425
+ return f"Error uploading photo: {e}", 500
426
  finally:
427
  os.remove(temp_path)
428
  products[index]['photos'] = new_photos_list
 
430
  try:
431
  price = float(price.replace(',', '.'))
432
  except ValueError:
433
+ return "Error: Price must be a number.", 400
434
  products[index]['price'] = price
435
  products[index]['description'] = description
436
  save_data(products)
 
447
  <meta charset="UTF-8">
448
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
449
  <title>Админ-панель</title>
450
+ <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
451
  <style>
452
  body {
453
+ font-family: 'Roboto', sans-serif;
454
+ margin: 0;
455
+ padding: 20px;
456
+ background-color: #f0f2f5;
 
457
  color: #333;
458
  }
459
+ .container {
460
+ max-width: 1200px;
461
+ margin: 0 auto;
462
+ }
463
+ h1, h2 {
464
+ color: #2c3e50;
465
+ }
466
  form {
467
  background-color: #fff;
468
  padding: 20px;
469
+ border-radius: 8px;
470
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
 
471
  margin-bottom: 20px;
472
  }
473
  label {
 
486
  button {
487
  margin-top: 15px;
488
  padding: 10px 15px;
489
+ background-color: #3498db;
490
  color: white;
491
  border: none;
492
  border-radius: 4px;
493
  cursor: pointer;
494
+ transition: background-color 0.3s;
495
  }
496
  button:hover {
497
+ background-color: #2980b9;
498
  }
499
  .product-list {
500
+ display: grid;
501
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
502
+ gap: 20px;
503
  }
504
  .product-item {
505
  background-color: #fff;
506
  border: 1px solid #ddd;
507
  padding: 15px;
508
+ border-radius: 8px;
509
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
510
+ }
511
+ .product-item img {
512
+ max-width: 100%;
513
+ height: auto;
514
+ border-radius: 4px;
515
  }
516
  .edit-form {
517
  margin-top: 10px;
518
  padding: 10px;
519
  border: 1px solid #ddd;
520
+ border-radius: 4px;
521
  background-color: #f9f9f9;
522
  }
523
  @media (max-width: 600px) {
524
+ .product-list {
525
+ grid-template-columns: 1fr;
 
 
 
 
 
 
 
 
 
 
 
 
526
  }
527
  }
528
  </style>
529
  </head>
530
  <body>
531
+ <div class="container">
532
+ <h1>Админ-панель</h1>
533
+ <form method="POST" enctype="multipart/form-data">
534
+ <h2>Добавление товара</h2>
535
+ <input type="hidden" name="action" value="add">
536
+ <label for="name">Название товара:</label>
537
+ <input type="text" id="name" name="name" required>
538
+ <label for="price">Цена:</label>
539
+ <input type="number" id="price" name="price" step="0.01" required>
540
+ <label for="description">Описание:</label>
541
+ <textarea id="description" name="description" rows="4" required></textarea>
542
+ <label for="photos">Фотографии товара (до 2):</label>
543
+ <input type="file" id="photos" name="photos" accept="image/*" multiple>
544
+ <button type="submit">Добавить товар</button>
545
+ </form>
546
+
547
+ <h2>Управление базой данных</h2>
548
+ <form method="POST" action="{{ url_for('backup') }}">
549
+ <button type="submit">Создать резервную копию</button>
550
+ </form>
551
+
552
+ <form method="GET" action="{{ url_for('download') }}">
553
+ <button type="submit">Скачать базу данных</button>
554
+ </form>
555
+
556
+ <h2>Список товаров</h2>
557
+ <div class="product-list">
558
+ {% for product in products %}
559
+ <div class="product-item">
560
+ <h3>{{ product['name'] }}</h3>
561
+ <p><strong>Цена:</strong> {{ product['price'] }} руб.</p>
562
+ <p><strong>Описание:</strong> {{ product['description'] }}</p>
563
+ {% if product.get('photos') and product['photos']|length > 0 %}
564
+ <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product['photos'][0] }}"
565
+ alt="{{ product['name'] }}">
566
+ {% endif %}
567
 
568
+ <details>
569
+ <summary>Редактировать</summary>
570
+ <form method="POST" enctype="multipart/form-data" class="edit-form">
571
+ <input type="hidden" name="action" value="edit">
572
+ <input type="hidden" name="index" value="{{ loop.index0 }}">
573
+ <label for="name">Название товара:</label>
574
+ <input type="text" id="name" name="name" value="{{ product['name'] }}" required>
575
+ <label for="price">Цена:</label>
576
+ <input type="number" id="price" name="price" step="0.01" value="{{ product['price'] }}" required>
577
+ <label for="description">Описание:</label>
578
+ <textarea id="description" name="description" rows="4" required>{{ product['description'] }}</textarea>
579
+ <label for="photos">Фотографии товара (до 2):</label>
580
+ <input type="file" id="photos" name="photos" accept="image/*" multiple>
581
+ <button type="submit">Сохранить изменения</button>
582
+ </form>
583
+ </details>
584
+
585
+ <form method="POST">
586
+ <input type="hidden" name="action" value="delete">
587
  <input type="hidden" name="index" value="{{ loop.index0 }}">
588
+ <button type="submit">Удалить</button>
 
 
 
 
 
 
 
 
589
  </form>
590
+ </div>
591
+ {% endfor %}
 
 
 
 
 
592
  </div>
 
593
  </div>
594
  </body>
595
  </html>
 
604
  @app.route('/download', methods=['GET'])
605
  def download():
606
  download_db_from_hf()
607
+ return send_file(DATA_FILE, as_attachment=True)
608
 
609
  if __name__ == '__main__':
610
  backup_thread = threading.Thread(target=periodic_backup, daemon=True)
 
613
  try:
614
  load_data()
615
  except Exception as e:
616
+ logging.error(f"Failed to load database at startup: {e}")
617
 
618
+ app.run(debug=True, host='0.0.0.0', port=7860)