flpolprojects commited on
Commit
0a69a07
·
verified ·
1 Parent(s): e7f3eb4

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +397 -419
app.py CHANGED
@@ -27,45 +27,60 @@ def load_data():
27
  download_db_from_hf()
28
  with open(DATA_FILE, 'r', encoding='utf-8') as file:
29
  data = json.load(file)
 
30
  if not isinstance(data, dict) or 'products' not in data or 'categories' not in data:
31
  if isinstance(data, list):
32
  return {'products': data, 'categories': []}
33
  else:
 
34
  return {'products': [], 'categories': []}
35
  return data
36
  except FileNotFoundError:
 
37
  return {'products': [], 'categories': []}
38
  except json.JSONDecodeError:
 
39
  return {'products': [], 'categories': []}
40
  except RepositoryNotFoundError:
 
41
  return {'products': [], 'categories': []}
42
  except HfHubHTTPError as e:
 
 
43
  try:
44
  with open(DATA_FILE, 'r', encoding='utf-8') as file:
45
  data = json.load(file)
 
46
  if not isinstance(data, dict) or 'products' not in data or 'categories' not in data:
47
  if isinstance(data, list):
48
  return {'products': data, 'categories': []}
49
  else:
 
50
  return {'products': [], 'categories': []}
51
  return data
52
  except FileNotFoundError:
 
53
  return {'products': [], 'categories': []}
54
  except json.JSONDecodeError:
 
55
  return {'products': [], 'categories': []}
56
  except Exception as e:
 
57
  return {'products': [], 'categories': []}
58
 
59
  def save_data(data):
60
  try:
61
  with open(DATA_FILE, 'w', encoding='utf-8') as file:
62
  json.dump(data, file, ensure_ascii=False, indent=4)
 
63
  upload_db_to_hf()
64
  except Exception as e:
 
65
  raise
66
 
67
  def upload_db_to_hf():
68
  if not HF_TOKEN_WRITE:
 
69
  return
70
  try:
71
  api = HfApi()
@@ -77,11 +92,13 @@ def upload_db_to_hf():
77
  token=HF_TOKEN_WRITE,
78
  commit_message=f"Автоматическое резервное копирование базы данных {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
79
  )
 
80
  except Exception as e:
81
  logging.error(f"Ошибка при загрузке резервной копии на Hugging Face: {e}")
82
 
83
  def download_db_from_hf():
84
  if not HF_TOKEN_READ:
 
85
  return
86
  try:
87
  hf_hub_download(
@@ -92,9 +109,12 @@ def download_db_from_hf():
92
  local_dir=".",
93
  local_dir_use_symlinks=False
94
  )
 
95
  except RepositoryNotFoundError:
 
96
  raise
97
  except Exception as e:
 
98
  raise
99
 
100
  def periodic_backup():
@@ -208,7 +228,7 @@ catalog_html = '''
208
  }
209
  .products-grid {
210
  display: grid;
211
- grid-template-columns: repeat(2, 1fr); /* Fixed 2 columns */
212
  gap: 15px;
213
  padding: 10px;
214
  }
@@ -320,9 +340,8 @@ catalog_html = '''
320
  box-shadow: 0 4px 15px rgba(239, 68, 68, 0.4);
321
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
322
  z-index: 1000;
323
- position: relative;
324
  }
325
- #cart-button span#cart-count {
326
  position: absolute;
327
  top: -5px;
328
  right: -5px;
@@ -332,10 +351,10 @@ catalog_html = '''
332
  padding: 2px 5px;
333
  font-size: 0.7em;
334
  display: none;
335
- line-height: 1;
336
- min-width: 18px;
337
  text-align: center;
338
- }
 
339
  .modal {
340
  display: none;
341
  position: fixed;
@@ -419,7 +438,7 @@ catalog_html = '''
419
 
420
  .quantity-input, .color-select {
421
  width: 100%;
422
- max-width: 150px;
423
  padding: 8px;
424
  border: 1px solid var(--secondary-color);
425
  border-radius: 8px;
@@ -427,15 +446,7 @@ catalog_html = '''
427
  margin: 5px 0;
428
  display: block;
429
  margin-bottom: 15px;
430
- box-sizing: border-box;
431
  }
432
- .modal-content label {
433
- font-weight: 500;
434
- display: block;
435
- margin-bottom: 5px;
436
- margin-top: 0;
437
- }
438
-
439
  .modal-content button {
440
  margin-top: 0;
441
  }
@@ -474,36 +485,8 @@ catalog_html = '''
474
  }
475
  }
476
  @media (max-width: 480px) {
477
- .products-grid {
478
- grid-template-columns: repeat(2, 1fr); /* Keep 2 columns even smaller */
479
- gap: 10px;
480
- }
481
- .product {
482
- padding: 10px;
483
- }
484
- .product-image {
485
- border-radius: 8px;
486
- }
487
- .product h2 {
488
- font-size: 0.9rem;
489
- margin: 8px 0 4px 0;
490
- }
491
- .product-price {
492
- font-size: 1rem;
493
- }
494
- .product-description {
495
- font-size: 0.7rem;
496
- margin-bottom: 10px;
497
- }
498
- .product-button {
499
- padding: 6px;
500
- font-size: 0.7rem;
501
- border-radius: 6px;
502
- margin: 3px 0;
503
- }
504
  .modal-content {
505
  margin: 15% auto;
506
- padding: 15px;
507
  }
508
  }
509
  </style>
@@ -609,17 +592,27 @@ catalog_html = '''
609
 
610
  function initializeSwiper() {
611
  if (document.querySelector('.swiper-container')) {
612
- new Swiper('.swiper-container', {
613
  slidesPerView: 1,
614
  spaceBetween: 20,
615
- loop: true,
616
  grabCursor: true,
617
  pagination: { el: '.swiper-pagination', clickable: true },
618
  navigation: { nextEl: '.swiper-button-next', prevEl: '.swiper-button-prev' },
619
  zoom: { maxRatio: 3 },
620
  keyboard: { enabled: true },
621
- mousewheel: { enabled: true },
622
  });
 
 
 
 
 
 
 
 
 
 
623
  } else {
624
  console.warn("Swiper container not found. Swiper not initialized.");
625
  }
@@ -728,7 +721,7 @@ catalog_html = '''
728
  ${photoUrl ? `<img src="${photoUrl}" alt="${item.name}">` : ''}
729
  <div class="item-details">
730
  <strong>${item.name}</strong>
731
- <p>${item.price} с × ${item.quantity} шт. (Цвет: ${item.color})</p>
732
  </div>
733
  </div>
734
  <span>${itemTotal} с</span>
@@ -842,6 +835,363 @@ detail_html = '''
842
  </div>
843
  '''
844
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
845
 
846
  @app.route('/')
847
  def catalog():
@@ -1036,378 +1386,6 @@ def admin():
1036
  logging.error(f"Ошибка при удалении товара: {e}")
1037
  return f"Ошибка при удалении товара: {e}", 500
1038
 
1039
- admin_html = '''
1040
- <!DOCTYPE html>
1041
- <html lang="ru">
1042
- <head>
1043
- <meta charset="UTF-8">
1044
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
1045
- <title>Админ-панель</title>
1046
- <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
1047
- <style>
1048
- :root {
1049
- --primary-color: #F48FB1;
1050
- --secondary-color: #CE93D8;
1051
- --text-color: #333;
1052
- --bg-color: #FCE4EC;
1053
- --light-text: #fff;
1054
- --shadow-color: rgba(0, 0, 0, 0.1);
1055
- }
1056
- body {
1057
- font-family: 'Poppins', sans-serif;
1058
- background: var(--bg-color);
1059
- color: var(--text-color);
1060
- padding: 20px;
1061
- }
1062
- .container {
1063
- max-width: 1200px;
1064
- margin: 0 auto;
1065
- }
1066
- .header {
1067
- display: flex;
1068
- align-items: center;
1069
- padding: 15px 0;
1070
- border-bottom: 1px solid var(--primary-color);
1071
- margin-bottom: 20px;
1072
- }
1073
- .header-logo {
1074
- width: 50px;
1075
- height: 50px;
1076
- border-radius: 50%;
1077
- object-fit: cover;
1078
- box-shadow: 0 4px 10px var(--shadow-color);
1079
- margin-right: 15px;
1080
- }
1081
- h1, h2 {
1082
- font-weight: 600;
1083
- margin-bottom: 20px;
1084
- color: var(--primary-color);
1085
- border-bottom: 1px dashed var(--secondary-color);
1086
- padding-bottom: 10px;
1087
- }
1088
- form {
1089
- background: var(--light-text);
1090
- padding: 20px;
1091
- border-radius: 15px;
1092
- box-shadow: 0 4px 15px var(--shadow-color);
1093
- margin-bottom: 30px;
1094
- }
1095
- label {
1096
- font-weight: 500;
1097
- margin-top: 15px;
1098
- display: block;
1099
- margin-bottom: 5px;
1100
- }
1101
- input, textarea, select {
1102
- width: 100%;
1103
- padding: 10px;
1104
- border: 1px solid var(--secondary-color);
1105
- border-radius: 8px;
1106
- font-size: 1rem;
1107
- transition: all 0.3s ease;
1108
- box-sizing: border-box;
1109
- }
1110
- input:focus, textarea:focus, select:focus {
1111
- border-color: var(--primary-color);
1112
- box-shadow: 0 0 5px rgba(244, 143, 177, 0.3);
1113
- outline: none;
1114
- }
1115
- button {
1116
- padding: 10px 15px;
1117
- border: none;
1118
- border-radius: 8px;
1119
- background-color: var(--primary-color);
1120
- color: var(--light-text);
1121
- font-weight: 500;
1122
- cursor: pointer;
1123
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
1124
- margin-top: 15px;
1125
- display: inline-block;
1126
- text-decoration: none;
1127
- }
1128
- button:hover {
1129
- background-color: #E91E63;
1130
- box-shadow: 0 4px 15px rgba(233, 30, 99, 0.4);
1131
- transform: translateY(-2px);
1132
- }
1133
- .delete-button {
1134
- background-color: #ef4444;
1135
- }
1136
- .delete-button:hover {
1137
- background-color: #dc2626;
1138
- box-shadow: 0 4px 15px rgba(220, 38, 38, 0.4);
1139
- }
1140
- .product-list, .category-list {
1141
- display: grid;
1142
- gap: 20px;
1143
- margin-top: 20px;
1144
- }
1145
- .product-item, .category-item {
1146
- background: var(--light-text);
1147
- padding: 20px;
1148
- border-radius: 15px;
1149
- box-shadow: 0 4px 15px var(--shadow-color);
1150
- }
1151
- .category-item {
1152
- display: flex;
1153
- justify-content: space-between;
1154
- align-items: center;
1155
- flex-wrap: wrap;
1156
- }
1157
- .category-item h3 {
1158
- margin: 0;
1159
- padding: 0;
1160
- border-bottom: none;
1161
- flex-grow: 1;
1162
- margin-right: 10px;
1163
- }
1164
- .category-item form {
1165
- margin: 0;
1166
- padding: 0;
1167
- box-shadow: none;
1168
- background: none;
1169
- }
1170
- .category-item button {
1171
- margin-top: 0;
1172
- }
1173
- .edit-form {
1174
- margin-top: 20px;
1175
- padding: 15px;
1176
- background: #f7fafc;
1177
- border-radius: 10px;
1178
- box-shadow: inset 0 1px 3px rgba(0,0,0,0.05);
1179
- }
1180
- .color-input-group {
1181
- display: flex;
1182
- gap: 10px;
1183
- margin-top: 5px;
1184
- align-items: center;
1185
- }
1186
- .color-input-group input {
1187
- flex-grow: 1;
1188
- margin-top: 0;
1189
- }
1190
- .remove-color-btn {
1191
- background-color: #ccc;
1192
- margin-top: 0;
1193
- padding: 5px 10px;
1194
- font-size: 0.9rem;
1195
- border-radius: 5px;
1196
- }
1197
- .remove-color-btn:hover {
1198
- background-color: #bbb;
1199
- box-shadow: none;
1200
- transform: none;
1201
- }
1202
- .add-color-btn {
1203
- background-color: var(--secondary-color);
1204
- }
1205
- .add-color-btn:hover {
1206
- background-color: #BA68C8;
1207
- }
1208
- .product-images-preview {
1209
- display: flex;
1210
- flex-wrap: wrap;
1211
- gap: 10px;
1212
- margin-top: 15px;
1213
- }
1214
- .product-images-preview img {
1215
- max-width: 80px;
1216
- height: auto;
1217
- border-radius: 8px;
1218
- border: 1px solid #eee;
1219
- box-shadow: 0 2px 5px rgba(0,0,0,0.05);
1220
- }
1221
- .db-buttons button {
1222
- margin-right: 10px;
1223
- }
1224
- details {
1225
- margin-top: 15px;
1226
- border: 1px solid #eee;
1227
- border-radius: 8px;
1228
- padding: 10px;
1229
- background-color: #fafafa;
1230
- }
1231
- summary {
1232
- font-weight: 600;
1233
- cursor: pointer;
1234
- color: var(--primary-color);
1235
- }
1236
- </style>
1237
- </head>
1238
- <body>
1239
- <div class="container">
1240
- <div class="header">
1241
- <img src="''' + LOGO_URL + '''" alt="Logo" class="header-logo">
1242
- <h1>Админ-панель</h1>
1243
- </div>
1244
-
1245
- <h1>Добавление товара</h1>
1246
- <form method="POST" enctype="multipart/form-data">
1247
- <input type="hidden" name="action" value="add">
1248
- <label for="add_name">Название товара:</label>
1249
- <input type="text" id="add_name" name="name" required>
1250
-
1251
- <label for="add_price">Цена (с):</label>
1252
- <input type="number" id="add_price" name="price" step="0.01" required>
1253
-
1254
- <label for="add_description">Описание:</label>
1255
- <textarea id="add_description" name="description" rows="4" required></textarea>
1256
-
1257
- <label for="add_category">Категория:</label>
1258
- <select id="add_category" name="category">
1259
- <option value="Без категории">Без категории</option>
1260
- {% for category in categories %}
1261
- <option value="{{ category|e }}">{{ category|e }}</option>
1262
- {% endfor %}
1263
- </select>
1264
-
1265
- <label for="add_photos">Фотографии (до 10, заменят старые при редактировании):</label>
1266
- <input type="file" id="add_photos" name="photos" accept="image/*" multiple>
1267
-
1268
- <label>Цвета:</label>
1269
- <div id="add-color-inputs">
1270
- <div class="color-input-group">
1271
- <input type="text" name="colors" placeholder="Например: Красный">
1272
- <button type="button" class="remove-color-btn" onclick="removeColorInput(this)">Удалить</button>
1273
- </div>
1274
- </div>
1275
- <button type="button" class="add-color-btn" onclick="addColorInput('add-color-inputs')">Добавить цвет</button>
1276
-
1277
- <button type="submit">Добавить товар</button>
1278
- </form>
1279
-
1280
- <h1>Управление категориями</h1>
1281
- <form method="POST">
1282
- <input type="hidden" name="action" value="add_category">
1283
- <label for="add_category_name">Название новой категории:</label>
1284
- <input type="text" id="add_category_name" name="category_name" required>
1285
- <button type="submit">Добавить</button>
1286
- </form>
1287
-
1288
- <h2>Список категорий</h2>
1289
- <div class="category-list">
1290
- {% for category in categories %}
1291
- <div class="category-item">
1292
- <h3>{{ category|e }}</h3>
1293
- <form method="POST" style="display: inline;">
1294
- <input type="hidden" name="action" value="delete_category">
1295
- <input type="hidden" name="category_name_to_delete" value="{{ category|e }}">
1296
- <button type="submit" class="delete-button" onclick="return confirm('Вы уверены, что хотите удалить категорию «{{ category|e }}»? Товары в этой категории будут перемещены в «Без категории».')">Удалить</button>
1297
- </form>
1298
- </div>
1299
- {% endfor %}
1300
- </div>
1301
-
1302
- <h2>Управление базой данных</h2>
1303
- <form method="POST" action="{{ url_for('backup') }}" style="display: inline-block; margin-right: 10px;">
1304
- <button type="submit">Создать резервную копию на HF</button>
1305
- </form>
1306
- <form method="GET" action="{{ url_for('download') }}" style="display: inline-block;">
1307
- <button type="submit">Скачать базу с HF</button>
1308
- </form>
1309
-
1310
-
1311
- <h2>Список товаров ({{ products|length }})</h2>
1312
- <div class="product-list">
1313
- {% for product in products %}
1314
- <div class="product-item">
1315
- <h3>{{ product['name']|e }}</h3>
1316
- <p><strong>Категория:</strong> {{ product.get('category', 'Без категории')|e }}</p>
1317
- <p><strong>Цена:</strong> {{ product['price'] }} с</p>
1318
- <p><strong>Описание:</strong> {{ product['description'][:150] }}{% if product['description']|length > 150 %}...{% endif %}</p>
1319
- <p><strong>Цвета:</strong> {{ product.get('colors', ['Нет цветов'])|join(', ')|e }}</p>
1320
- {% if product.get('photos') and product['photos']|length > 0 %}
1321
- <div class="product-images-preview">
1322
- {% for photo in product['photos'] %}
1323
- <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo|e }}"
1324
- alt="{{ product['name']|e }}"
1325
- loading="lazy">
1326
- {% endfor %}
1327
- </div>
1328
- {% endif %}
1329
- <details>
1330
- <summary>Редактировать</summary>
1331
- <form method="POST" enctype="multipart/form-data" class="edit-form">
1332
- <input type="hidden" name="action" value="edit">
1333
- <input type="hidden" name="index" value="{{ loop.index0 }}">
1334
- <label for="edit_name_{{ loop.index0 }}">Название:</label>
1335
- <input type="text" id="edit_name_{{ loop.index0 }}" name="name" value="{{ product['name']|e }}" required>
1336
-
1337
- <label for="edit_price_{{ loop.index0 }}">Цена (с):</label>
1338
- <input type="number" id="edit_price_{{ loop.index0 }}" name="price" step="0.01" value="{{ product['price'] }}" required>
1339
-
1340
- <label for="edit_description_{{ loop.index0 }}">Описание:</label>
1341
- <textarea id="edit_description_{{ loop.index0 }}" name="description" rows="4" required>{{ product['description']|e }}</textarea>
1342
-
1343
- <label for="edit_category_{{ loop.index0 }}">Категория:</label>
1344
- <select id="edit_category_{{ loop.index0 }}" name="category">
1345
- <option value="Без категории" {% if product.get('category', 'Без категории') == 'Без категории' %}selected{% endif %}>Без категории</option>
1346
- {% for category in categories %}
1347
- <option value="{{ category|e }}" {% if product.get('category') == category %}selected{% endif %}>{{ category|e }}</option>
1348
- {% endfor %}
1349
- </select>
1350
-
1351
- <label for="edit_photos_{{ loop.index0 }}">Загрузить новые фотографии (заменят текущие):</label>
1352
- <input type="file" id="edit_photos_{{ loop.index0 }}" name="photos" accept="image/*" multiple>
1353
-
1354
- <label>Цвета:</label>
1355
- <div id="edit-color-inputs-{{ loop.index0 }}">
1356
- {% for color in product.get('colors', []) %}
1357
- <div class="color-input-group">
1358
- <input type="text" name="colors" value="{{ color|e }}">
1359
- <button type="button" class="remove-color-btn" onclick="removeColorInput(this)">Удалить</button>
1360
- </div>
1361
- {% endfor %}
1362
- </div>
1363
- <button type="button" class="add-color-btn" onclick="addColorInput('edit-color-inputs-{{ loop.index0 }}')">Добавить цвет</button>
1364
-
1365
-
1366
- <button type="submit">Сохранить изменения</button>
1367
- </form>
1368
- </details>
1369
- <form method="POST" style="margin-top: 10px;">
1370
- <input type="hidden" name="action" value="delete">
1371
- <input type="hidden" name="index" value="{{ loop.index0 }}">
1372
- <button type="submit" class="delete-button" onclick="return confirm('Вы уверены, что хотите удалить товар «{{ product['name']|e }}»?')">Удалить товар</button>
1373
- </form>
1374
- </div>
1375
- {% endfor %}
1376
- </div>
1377
- </div>
1378
- <script>
1379
- function addColorInput(containerId) {
1380
- const container = document.getElementById(containerId);
1381
- const newInputGroup = document.createElement('div');
1382
- newInputGroup.className = 'color-input-group';
1383
- newInputGroup.innerHTML = `
1384
- <input type="text" name="colors" placeholder="Например: Красный">
1385
- <button type="button" class="remove-color-btn" onclick="removeColorInput(this)">Удалить</button>
1386
- `;
1387
- container.appendChild(newInputGroup);
1388
- }
1389
-
1390
- function removeColorInput(buttonElement) {
1391
- buttonElement.closest('.color-input-group').remove();
1392
- }
1393
- document.addEventListener('DOMContentLoaded', function() {
1394
- // Ensure at least one color input exists on load for new product form
1395
- const addColorContainer = document.getElementById('add-color-inputs');
1396
- if (addColorContainer && addColorContainer.children.length === 0) {
1397
- addColorInput('add-color-inputs');
1398
- }
1399
-
1400
- // Ensure at least one color input exists on load for edit forms
1401
- document.querySelectorAll('.edit-form [id^="edit-color-inputs-"]').forEach(container => {
1402
- if (container.children.length === 0) {
1403
- addColorInput(container.id);
1404
- }
1405
- });
1406
- });
1407
- </script>
1408
- </body>
1409
- </html>
1410
- '''
1411
  return render_template_string(admin_html, products=products, categories=categories, repo_id=REPO_ID, LOGO_URL=LOGO_URL)
1412
 
1413
  @app.route('/backup', methods=['POST'])
 
27
  download_db_from_hf()
28
  with open(DATA_FILE, 'r', encoding='utf-8') as file:
29
  data = json.load(file)
30
+ logging.info("Данные успешно загружены из JSON")
31
  if not isinstance(data, dict) or 'products' not in data or 'categories' not in data:
32
  if isinstance(data, list):
33
  return {'products': data, 'categories': []}
34
  else:
35
+ logging.warning("Неожиданный формат данных в JSON.")
36
  return {'products': [], 'categories': []}
37
  return data
38
  except FileNotFoundError:
39
+ logging.warning(f"Локальный файл базы данных ({DATA_FILE}) не найден после скачивания.")
40
  return {'products': [], 'categories': []}
41
  except json.JSONDecodeError:
42
+ logging.error("Ошибка: Невозможно декодировать JSON файл.")
43
  return {'products': [], 'categories': []}
44
  except RepositoryNotFoundError:
45
+ logging.error(f"Репозиторий '{REPO_ID}' не найден на Hugging Face. Создание локальной базы данных.")
46
  return {'products': [], 'categories': []}
47
  except HfHubHTTPError as e:
48
+ logging.error(f"Ошибка HTTP при скачивании из Hugging Face: {e}")
49
+ logging.warning(f"Попытка загрузить локальный файл '{DATA_FILE}' если существует.")
50
  try:
51
  with open(DATA_FILE, 'r', encoding='utf-8') as file:
52
  data = json.load(file)
53
+ logging.info("Данные успешно загружены из локального JSON после ошибки HF.")
54
  if not isinstance(data, dict) or 'products' not in data or 'categories' not in data:
55
  if isinstance(data, list):
56
  return {'products': data, 'categories': []}
57
  else:
58
+ logging.warning("Неожиданный формат данных в локальном JSON.")
59
  return {'products': [], 'categories': []}
60
  return data
61
  except FileNotFoundError:
62
+ logging.warning(f"Локальный файл '{DATA_FILE}' также не найден.")
63
  return {'products': [], 'categories': []}
64
  except json.JSONDecodeError:
65
+ logging.error("Ошибка: Невозможно декодировать локальный JSON файл.")
66
  return {'products': [], 'categories': []}
67
  except Exception as e:
68
+ logging.error(f"Произошла неизвестная ошибка при загрузке данных: {e}")
69
  return {'products': [], 'categories': []}
70
 
71
  def save_data(data):
72
  try:
73
  with open(DATA_FILE, 'w', encoding='utf-8') as file:
74
  json.dump(data, file, ensure_ascii=False, indent=4)
75
+ logging.info("Данные успешно сохранены в JSON")
76
  upload_db_to_hf()
77
  except Exception as e:
78
+ logging.error(f"Ошибка при сохранении данных: {e}")
79
  raise
80
 
81
  def upload_db_to_hf():
82
  if not HF_TOKEN_WRITE:
83
+ logging.warning("HF_TOKEN не установлен. Пропуск загрузки на Hugging Face.")
84
  return
85
  try:
86
  api = HfApi()
 
92
  token=HF_TOKEN_WRITE,
93
  commit_message=f"Автоматическое резервное копирование базы данных {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
94
  )
95
+ logging.info("Резервная копия JSON базы успешно загружена на Hugging Face.")
96
  except Exception as e:
97
  logging.error(f"Ошибка при загрузке резервной копии на Hugging Face: {e}")
98
 
99
  def download_db_from_hf():
100
  if not HF_TOKEN_READ:
101
+ logging.warning("HF_TOKEN_READ не установлен. Пропуск скачивания из Hugging Face.")
102
  return
103
  try:
104
  hf_hub_download(
 
109
  local_dir=".",
110
  local_dir_use_symlinks=False
111
  )
112
+ logging.info("JSON база успешно скачана из Hugging Face.")
113
  except RepositoryNotFoundError:
114
+ logging.warning(f"Репозиторий '{REPO_ID}' не найден. Скачивание невозможно.")
115
  raise
116
  except Exception as e:
117
+ logging.error(f"Ошибка при скачивании JSON базы из Hugging Face: {e}")
118
  raise
119
 
120
  def periodic_backup():
 
228
  }
229
  .products-grid {
230
  display: grid;
231
+ grid-template-columns: repeat(2, 1fr);
232
  gap: 15px;
233
  padding: 10px;
234
  }
 
340
  box-shadow: 0 4px 15px rgba(239, 68, 68, 0.4);
341
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
342
  z-index: 1000;
 
343
  }
344
+ #cart-button #cart-count {
345
  position: absolute;
346
  top: -5px;
347
  right: -5px;
 
351
  padding: 2px 5px;
352
  font-size: 0.7em;
353
  display: none;
354
+ min-width: 20px;
 
355
  text-align: center;
356
+ line-height: 1.2;
357
+ }
358
  .modal {
359
  display: none;
360
  position: fixed;
 
438
 
439
  .quantity-input, .color-select {
440
  width: 100%;
441
+ max-width: 200px;
442
  padding: 8px;
443
  border: 1px solid var(--secondary-color);
444
  border-radius: 8px;
 
446
  margin: 5px 0;
447
  display: block;
448
  margin-bottom: 15px;
 
449
  }
 
 
 
 
 
 
 
450
  .modal-content button {
451
  margin-top: 0;
452
  }
 
485
  }
486
  }
487
  @media (max-width: 480px) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
488
  .modal-content {
489
  margin: 15% auto;
 
490
  }
491
  }
492
  </style>
 
592
 
593
  function initializeSwiper() {
594
  if (document.querySelector('.swiper-container')) {
595
+ const swiper = new Swiper('.swiper-container', {
596
  slidesPerView: 1,
597
  spaceBetween: 20,
598
+ loop: document.querySelectorAll('.swiper-slide').length > 1,
599
  grabCursor: true,
600
  pagination: { el: '.swiper-pagination', clickable: true },
601
  navigation: { nextEl: '.swiper-button-next', prevEl: '.swiper-button-prev' },
602
  zoom: { maxRatio: 3 },
603
  keyboard: { enabled: true },
604
+ mousewheel: { enabled: true },
605
  });
606
+ // Hide navigation if there's only one slide
607
+ if (document.querySelectorAll('.swiper-slide').length <= 1) {
608
+ document.querySelector('.swiper-button-next').style.display = 'none';
609
+ document.querySelector('.swiper-button-prev').style.display = 'none';
610
+ document.querySelector('.swiper-pagination').style.display = 'none';
611
+ } else {
612
+ document.querySelector('.swiper-button-next').style.display = '';
613
+ document.querySelector('.swiper-button-prev').style.display = '';
614
+ document.querySelector('.swiper-pagination').style.display = '';
615
+ }
616
  } else {
617
  console.warn("Swiper container not found. Swiper not initialized.");
618
  }
 
721
  ${photoUrl ? `<img src="${photoUrl}" alt="${item.name}">` : ''}
722
  <div class="item-details">
723
  <strong>${item.name}</strong>
724
+ <p>${item.price} с × ${item.quantity} (Цвет: ${item.color})</p>
725
  </div>
726
  </div>
727
  <span>${itemTotal} с</span>
 
835
  </div>
836
  '''
837
 
838
+ admin_html = '''
839
+ <!DOCTYPE html>
840
+ <html lang="ru">
841
+ <head>
842
+ <meta charset="UTF-8">
843
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
844
+ <title>Админ-панель</title>
845
+ <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
846
+ <style>
847
+ :root {
848
+ --primary-color: #F48FB1;
849
+ --secondary-color: #CE93D8;
850
+ --text-color: #333;
851
+ --bg-color: #FCE4EC;
852
+ --light-text: #fff;
853
+ --shadow-color: rgba(0, 0, 0, 0.1);
854
+ }
855
+ body {
856
+ font-family: 'Poppins', sans-serif;
857
+ background: var(--bg-color);
858
+ color: var(--text-color);
859
+ padding: 20px;
860
+ }
861
+ .container {
862
+ max-width: 1200px;
863
+ margin: 0 auto;
864
+ }
865
+ .header {
866
+ display: flex;
867
+ align-items: center;
868
+ padding: 15px 0;
869
+ border-bottom: 1px solid var(--primary-color);
870
+ margin-bottom: 20px;
871
+ }
872
+ .header-logo {
873
+ width: 50px;
874
+ height: 50px;
875
+ border-radius: 50%;
876
+ object-fit: cover;
877
+ box-shadow: 0 4px 10px var(--shadow-color);
878
+ margin-right: 15px;
879
+ }
880
+ h1, h2 {
881
+ font-weight: 600;
882
+ margin-bottom: 20px;
883
+ color: var(--primary-color);
884
+ border-bottom: 1px dashed var(--secondary-color);
885
+ padding-bottom: 10px;
886
+ }
887
+ form {
888
+ background: var(--light-text);
889
+ padding: 20px;
890
+ border-radius: 15px;
891
+ box-shadow: 0 4px 15px var(--shadow-color);
892
+ margin-bottom: 30px;
893
+ }
894
+ label {
895
+ font-weight: 500;
896
+ margin-top: 15px;
897
+ display: block;
898
+ margin-bottom: 5px;
899
+ }
900
+ input, textarea, select {
901
+ width: 100%;
902
+ padding: 10px;
903
+ border: 1px solid var(--secondary-color);
904
+ border-radius: 8px;
905
+ font-size: 1rem;
906
+ transition: all 0.3s ease;
907
+ box-sizing: border-box;
908
+ }
909
+ input:focus, textarea:focus, select:focus {
910
+ border-color: var(--primary-color);
911
+ box-shadow: 0 0 5px rgba(244, 143, 177, 0.3);
912
+ outline: none;
913
+ }
914
+ button {
915
+ padding: 10px 15px;
916
+ border: none;
917
+ border-radius: 8px;
918
+ background-color: var(--primary-color);
919
+ color: var(--light-text);
920
+ font-weight: 500;
921
+ cursor: pointer;
922
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
923
+ margin-top: 15px;
924
+ display: inline-block;
925
+ text-decoration: none;
926
+ }
927
+ button:hover {
928
+ background-color: #E91E63;
929
+ box-shadow: 0 4px 15px rgba(233, 30, 99, 0.4);
930
+ transform: translateY(-2px);
931
+ }
932
+ .delete-button {
933
+ background-color: #ef4444;
934
+ }
935
+ .delete-button:hover {
936
+ background-color: #dc2626;
937
+ box-shadow: 0 4px 15px rgba(220, 38, 38, 0.4);
938
+ }
939
+ .product-list, .category-list {
940
+ display: grid;
941
+ gap: 20px;
942
+ margin-top: 20px;
943
+ }
944
+ .product-item, .category-item {
945
+ background: var(--light-text);
946
+ padding: 20px;
947
+ border-radius: 15px;
948
+ box-shadow: 0 4px 15px var(--shadow-color);
949
+ }
950
+ .category-item {
951
+ display: flex;
952
+ justify-content: space-between;
953
+ align-items: center;
954
+ flex-wrap: wrap;
955
+ }
956
+ .category-item h3 {
957
+ margin: 0;
958
+ padding: 0;
959
+ border-bottom: none;
960
+ flex-grow: 1;
961
+ margin-right: 10px;
962
+ }
963
+ .category-item form {
964
+ margin: 0;
965
+ padding: 0;
966
+ box-shadow: none;
967
+ background: none;
968
+ }
969
+ .category-item button {
970
+ margin-top: 0;
971
+ }
972
+ .edit-form {
973
+ margin-top: 20px;
974
+ padding: 15px;
975
+ background: #f7fafc;
976
+ border-radius: 10px;
977
+ box-shadow: inset 0 1px 3px rgba(0,0,0,0.05);
978
+ }
979
+ .color-input-group {
980
+ display: flex;
981
+ gap: 10px;
982
+ margin-top: 5px;
983
+ align-items: center;
984
+ }
985
+ .color-input-group input {
986
+ flex-grow: 1;
987
+ margin-top: 0;
988
+ }
989
+ .remove-color-btn {
990
+ background-color: #ccc;
991
+ margin-top: 0;
992
+ padding: 5px 10px;
993
+ font-size: 0.9rem;
994
+ border-radius: 5px;
995
+ }
996
+ .remove-color-btn:hover {
997
+ background-color: #bbb;
998
+ box-shadow: none;
999
+ transform: none;
1000
+ }
1001
+ .add-color-btn {
1002
+ background-color: var(--secondary-color);
1003
+ }
1004
+ .add-color-btn:hover {
1005
+ background-color: #BA68C8;
1006
+ }
1007
+ .product-images-preview {
1008
+ display: flex;
1009
+ flex-wrap: wrap;
1010
+ gap: 10px;
1011
+ margin-top: 15px;
1012
+ }
1013
+ .product-images-preview img {
1014
+ max-width: 80px;
1015
+ height: auto;
1016
+ border-radius: 8px;
1017
+ border: 1px solid #eee;
1018
+ box-shadow: 0 2px 5px rgba(0,0,0,0.05);
1019
+ }
1020
+ .db-buttons button {
1021
+ margin-right: 10px;
1022
+ }
1023
+ details {
1024
+ margin-top: 15px;
1025
+ border: 1px solid #eee;
1026
+ border-radius: 8px;
1027
+ padding: 10px;
1028
+ background-color: #fafafa;
1029
+ }
1030
+ summary {
1031
+ font-weight: 600;
1032
+ cursor: pointer;
1033
+ color: var(--primary-color);
1034
+ }
1035
+ </style>
1036
+ </head>
1037
+ <body>
1038
+ <div class="container">
1039
+ <div class="header">
1040
+ <img src="''' + LOGO_URL + '''" alt="Logo" class="header-logo">
1041
+ <h1>Админ-панель</h1>
1042
+ </div>
1043
+
1044
+ <h1>Добавление товара</h1>
1045
+ <form method="POST" enctype="multipart/form-data">
1046
+ <input type="hidden" name="action" value="add">
1047
+ <label for="add_name">Название товара:</label>
1048
+ <input type="text" id="add_name" name="name" required>
1049
+
1050
+ <label for="add_price">Цена (с):</label>
1051
+ <input type="number" id="add_price" name="price" step="0.01" required>
1052
+
1053
+ <label for="add_description">Описание:</label>
1054
+ <textarea id="add_description" name="description" rows="4" required></textarea>
1055
+
1056
+ <label for="add_category">Категория:</label>
1057
+ <select id="add_category" name="category">
1058
+ <option value="Без категории">Без категории</option>
1059
+ {% for category in categories %}
1060
+ <option value="{{ category|e }}">{{ category|e }}</option>
1061
+ {% endfor %}
1062
+ </select>
1063
+
1064
+ <label for="add_photos">Фотографии (до 10, заменят старые при редактировании):</label>
1065
+ <input type="file" id="add_photos" name="photos" accept="image/*" multiple>
1066
+
1067
+ <label>Цвета:</label>
1068
+ <div id="add-color-inputs">
1069
+ <div class="color-input-group">
1070
+ <input type="text" name="colors" placeholder="Например: Красный">
1071
+ </div>
1072
+ </div>
1073
+ <button type="button" class="add-color-btn" onclick="addColorInput('add-color-inputs')">Добавить цвет</button>
1074
+
1075
+ <button type="submit">Добавить товар</button>
1076
+ </form>
1077
+
1078
+ <h1>Управление категориями</h1>
1079
+ <form method="POST">
1080
+ <input type="hidden" name="action" value="add_category">
1081
+ <label for="add_category_name">Название новой категории:</label>
1082
+ <input type="text" id="add_category_name" name="category_name" required>
1083
+ <button type="submit">Добавить</button>
1084
+ </form>
1085
+
1086
+ <h2>Список категорий</h2>
1087
+ <div class="category-list">
1088
+ {% for category in categories %}
1089
+ <div class="category-item">
1090
+ <h3>{{ category|e }}</h3>
1091
+ <form method="POST" style="display: inline;">
1092
+ <input type="hidden" name="action" value="delete_category">
1093
+ <input type="hidden" name="category_name_to_delete" value="{{ category|e }}">
1094
+ <button type="submit" class="delete-button" onclick="return confirm('Вы уверены, что хотите удалить категорию «{{ category|e }}»? Товары в этой категории будут перемещены в «Без категории».')">Удалить</button>
1095
+ </form>
1096
+ </div>
1097
+ {% endfor %}
1098
+ </div>
1099
+
1100
+ <h2>Управление базой данных</h2>
1101
+ <form method="POST" action="{{ url_for('backup') }}" style="display: inline-block;">
1102
+ <button type="submit">Создать резервную копию на HF</button>
1103
+ </form>
1104
+ <form method="GET" action="{{ url_for('download') }}" style="display: inline-block;">
1105
+ <button type="submit">Скачать базу с HF</button>
1106
+ </form>
1107
+
1108
+
1109
+ <h2>Список товаров ({{ products|length }})</h2>
1110
+ <div class="product-list">
1111
+ {% for product in products %}
1112
+ <div class="product-item">
1113
+ <h3>{{ product['name']|e }}</h3>
1114
+ <p><strong>Категория:</strong> {{ product.get('category', 'Без категории')|e }}</p>
1115
+ <p><strong>Цена:</strong> {{ product['price'] }} с</p>
1116
+ <p><strong>Описание:</strong> {{ product['description'][:150] }}{% if product['description']|length > 150 %}...{% endif %}</p>
1117
+ <p><strong>Цвета:</strong> {{ product.get('colors', ['Нет цветов'])|join(', ')|e }}</p>
1118
+ {% if product.get('photos') and product['photos']|length > 0 %}
1119
+ <div class="product-images-preview">
1120
+ {% for photo in product['photos'] %}
1121
+ <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo|e }}"
1122
+ alt="{{ product['name']|e }}"
1123
+ loading="lazy">
1124
+ {% endfor %}
1125
+ </div>
1126
+ {% endif %}
1127
+ <details>
1128
+ <summary>Редактировать</summary>
1129
+ <form method="POST" enctype="multipart/form-data" class="edit-form">
1130
+ <input type="hidden" name="action" value="edit">
1131
+ <input type="hidden" name="index" value="{{ loop.index0 }}">
1132
+ <label for="edit_name_{{ loop.index0 }}">Название:</label>
1133
+ <input type="text" id="edit_name_{{ loop.index0 }}" name="name" value="{{ product['name']|e }}" required>
1134
+
1135
+ <label for="edit_price_{{ loop.index0 }}">Цена (с):</label>
1136
+ <input type="number" id="edit_price_{{ loop.index0 }}" name="price" step="0.01" value="{{ product['price'] }}" required>
1137
+
1138
+ <label for="edit_description_{{ loop.index0 }}">Описание:</label>
1139
+ <textarea id="edit_description_{{ loop.index0 }}" name="description" rows="4" required>{{ product['description']|e }}</textarea>
1140
+
1141
+ <label for="edit_category_{{ loop.index0 }}">Категория:</label>
1142
+ <select id="edit_category_{{ loop.index0 }}" name="category">
1143
+ <option value="Без категории" {% if product.get('category', 'Без категории') == 'Без категории' %}selected{% endif %}>Без категории</option>
1144
+ {% for category in categories %}
1145
+ <option value="{{ category|e }}" {% if product.get('category') == category %}selected{% endif %}>{{ category|e }}</option>
1146
+ {% endfor %}
1147
+ </select>
1148
+
1149
+ <label for="edit_photos_{{ loop.index0 }}">Загрузить новые фотографии (заменят текущие):</label>
1150
+ <input type="file" id="edit_photos_{{ loop.index0 }}" name="photos" accept="image/*" multiple>
1151
+
1152
+ <label>Цвета:</label>
1153
+ <div id="edit-color-inputs-{{ loop.index0 }}">
1154
+ {% for color in product.get('colors', []) %}
1155
+ <div class="color-input-group">
1156
+ <input type="text" name="colors" value="{{ color|e }}">
1157
+ <button type="button" class="remove-color-btn" onclick="removeColorInput(this)">Удалить</button>
1158
+ </div>
1159
+ {% endfor %}
1160
+ </div>
1161
+ <button type="button" class="add-color-btn" onclick="addColorInput('edit-color-inputs-{{ loop.index0 }}')">Добавить цвет</button>
1162
+
1163
+
1164
+ <button type="submit">Сохранить изменения</button>
1165
+ </form>
1166
+ </details>
1167
+ <form method="POST" style="margin-top: 10px;">
1168
+ <input type="hidden" name="action" value="delete">
1169
+ <input type="hidden" name="index" value="{{ loop.index0 }}">
1170
+ <button type="submit" class="delete-button" onclick="return confirm('Вы уверены, что хотите удалить товар «{{ product['name']|e }}»?')">Удалить товар</button>
1171
+ </form>
1172
+ </div>
1173
+ {% endfor %}
1174
+ </div>
1175
+ </div>
1176
+ <script>
1177
+ function addColorInput(containerId) {
1178
+ const container = document.getElementById(containerId);
1179
+ const newInputGroup = document.createElement('div');
1180
+ newInputGroup.className = 'color-input-group';
1181
+ newInputGroup.innerHTML = `
1182
+ <input type="text" name="colors" placeholder="Например: Красный">
1183
+ <button type="button" class="remove-color-btn" onclick="removeColorInput(this)">Удалить</button>
1184
+ `;
1185
+ container.appendChild(newInputGroup);
1186
+ }
1187
+
1188
+ function removeColorInput(buttonElement) {
1189
+ buttonElement.closest('.color-input-group').remove();
1190
+ }
1191
+ </script>
1192
+ </body>
1193
+ </html>
1194
+ '''
1195
 
1196
  @app.route('/')
1197
  def catalog():
 
1386
  logging.error(f"Ошибка при удалении товара: {e}")
1387
  return f"Ошибка при удалении товара: {e}", 500
1388
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1389
  return render_template_string(admin_html, products=products, categories=categories, repo_id=REPO_ID, LOGO_URL=LOGO_URL)
1390
 
1391
  @app.route('/backup', methods=['POST'])