flpolprojects commited on
Commit
51849f1
·
verified ·
1 Parent(s): a9bbef6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +282 -178
app.py CHANGED
@@ -1,4 +1,4 @@
1
- from flask import Flask, render_template_string, request, redirect, url_for, send_file
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 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,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("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,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"Error saving data: {e}")
47
  raise
48
 
49
  def upload_db_to_hf():
@@ -52,14 +52,13 @@ 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,18 +70,18 @@ def download_db_from_hf():
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,9 +92,9 @@ 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,15 +102,15 @@ def catalog():
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,24 +121,29 @@ def catalog():
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,14 +154,11 @@ def catalog():
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,10 +169,10 @@ def catalog():
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,63 +180,152 @@ def catalog():
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,54 +334,68 @@ def catalog():
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,6 +404,7 @@ def catalog():
300
  });
301
  }
302
 
 
303
  window.onclick = function(event) {
304
  if (event.target == document.getElementById('productModal')) {
305
  closeModal();
@@ -317,9 +422,9 @@ def product_detail(index):
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,12 +440,14 @@ def product_detail(index):
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,7 +464,7 @@ def admin():
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,19 +479,19 @@ def admin():
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,9 +507,10 @@ def admin():
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,12 +525,12 @@ def admin():
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,7 +538,7 @@ def admin():
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,27 +555,21 @@ def admin():
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,110 +588,112 @@ def admin():
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,7 +708,7 @@ def backup():
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,6 +717,6 @@ if __name__ == '__main__':
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)
 
1
+ from flask import Flask, render_template_string, request, redirect, url_for
2
  import json
3
  import os
4
  import logging
 
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
  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
  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
  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
  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
  <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
  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
  }
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
  .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
  .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
  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
  <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
  });
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
  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
  </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
  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
  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
  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
  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
  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
  <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
  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
  @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
  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)