Shveiauto commited on
Commit
b72039b
·
verified ·
1 Parent(s): 4e5b23d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +229 -147
app.py CHANGED
@@ -1,5 +1,6 @@
1
 
2
- from flask import Flask, render_template_string, request, redirect, url_for, session, send_file
 
3
  import json
4
  import os
5
  import logging
@@ -276,97 +277,107 @@ def catalog():
276
  <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
277
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.css">
278
  <style>
279
- /* Стили остаются без изменений, как в предыдущем ответе */
280
  * { margin: 0; padding: 0; box-sizing: border-box; }
281
- body { font-family: 'Poppins', sans-serif; background: #f9f9f9; color: #333; line-height: 1.6; transition: background 0.3s, color 0.3s; }
282
- body.dark-mode { background: #1a202c; color: #e2e8f0; }
283
  .container { max-width: 1300px; margin: 0 auto; padding: 20px; }
284
- .header { display: flex; justify-content: space-between; align-items: center; padding: 15px 0; border-bottom: 1px solid #eee; }
285
- body.dark-mode .header { border-bottom-color: #4a5568; }
286
- .header h1 { font-size: 1.8rem; font-weight: 600; color: #e53e3e; } /* Логотип Soola */
287
  .auth-links { display: flex; gap: 15px; align-items: center; }
288
- .auth-links a { color: #3b82f6; text-decoration: none; font-weight: 500; }
289
  .auth-links a:hover { text-decoration: underline; }
290
- body.dark-mode .auth-links a { color: #63b3ed; }
291
  .auth-links span { font-weight: 500; }
292
- body.dark-mode .auth-links span { color: #cbd5e0;}
293
- .theme-toggle { background: none; border: none; font-size: 1.5rem; cursor: pointer; color: #718096; transition: color 0.3s ease; }
294
- .theme-toggle:hover { color: #3b82f6; }
295
- body.dark-mode .theme-toggle { color: #a0aec0; }
296
- body.dark-mode .theme-toggle:hover { color: #63b3ed; }
297
- .store-address { padding: 15px; text-align: center; background-color: #fff; margin: 20px 0; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); font-size: 1rem; color: #555; }
298
- body.dark-mode .store-address { background-color: #2d3748; color: #cbd5e0; }
 
 
299
  .filters-container { margin: 20px 0; display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }
300
  .search-container { margin: 20px 0; text-align: center; }
301
- #search-input { width: 90%; max-width: 600px; padding: 12px 18px; font-size: 1rem; border: 1px solid #e2e8f0; border-radius: 25px; outline: none; box-shadow: 0 2px 5px rgba(0,0,0,0.05); transition: all 0.3s ease; }
302
- body.dark-mode #search-input { background-color: #2d3748; border-color: #4a5568; color: #e2e8f0; }
303
- #search-input:focus { border-color: #e53e3e; box-shadow: 0 0 0 3px rgba(229, 62, 62, 0.2); }
304
- body.dark-mode #search-input:focus { border-color: #fc8181; box-shadow: 0 0 0 3px rgba(252, 129, 129, 0.3); }
305
- .category-filter { padding: 8px 16px; border: 1px solid #e2e8f0; border-radius: 20px; background-color: #fff; cursor: pointer; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); font-size: 0.9rem; font-weight: 400; }
306
- body.dark-mode .category-filter { background-color: #2d3748; border-color: #4a5568; color: #cbd5e0; }
307
- .category-filter.active, .category-filter:hover { background-color: #e53e3e; color: white; border-color: #e53e3e; box-shadow: 0 2px 10px rgba(229, 62, 62, 0.3); }
308
- body.dark-mode .category-filter.active, body.dark-mode .category-filter:hover { background-color: #fc8181; border-color: #fc8181; color: #1a202c; box-shadow: 0 2px 10px rgba(252, 129, 129, 0.4); }
 
 
309
  .products-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 20px; padding: 10px; }
310
  @media (min-width: 600px) { .products-grid { grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); } }
311
- .product { background: #fff; border-radius: 15px; padding: 0; /* Убираем основной padding */ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08); transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.3s ease; overflow: hidden; display: flex; flex-direction: column; justify-content: space-between; height: 100%; }
312
- body.dark-mode .product { background: #2d3748; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); }
313
  .product:hover { transform: translateY(-5px) scale(1.02); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12); }
314
  body.dark-mode .product:hover { box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3); }
315
- .product-image { width: 100%; aspect-ratio: 1 / 1; /* Делаем квадратным */ background-color: #fff; border-radius: 10px 10px 0 0; /* Скругление только сверху */ overflow: hidden; display: flex; justify-content: center; align-items: center; margin-bottom: 0; /* Убираем отступ снизу */ }
316
  .product-image img { max-width: 100%; max-height: 100%; object-fit: contain; transition: transform 0.3s ease; }
317
  .product-image img:hover { transform: scale(1.08); }
318
- .product-info { padding: 15px; flex-grow: 1; display: flex; flex-direction: column; justify-content: center; /* Центрируем контент */ }
319
- .product h2 { font-size: 1.1rem; font-weight: 600; margin: 0 0 8px 0; text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: #333; }
320
- body.dark-mode .product h2 { color: #e2e8f0; }
321
- .product-price { font-size: 1.2rem; color: #e53e3e; font-weight: 700; text-align: center; margin: 5px 0; }
322
- body.dark-mode .product-price { color: #fc8181; }
323
- .product-description { font-size: 0.85rem; color: #718096; text-align: center; margin-bottom: 15px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
324
- body.dark-mode .product-description { color: #a0aec0; }
325
- .product-actions { padding: 0 15px 15px 15px; /* Отступы только для кнопок */ display: flex; flex-direction: column; gap: 8px; }
326
- .product-button { display: block; width: 100%; padding: 10px; border: none; border-radius: 8px; background-color: #e53e3e; color: white; font-size: 0.9rem; font-weight: 500; cursor: pointer; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); text-align: center; text-decoration: none; }
327
- .product-button:hover { background-color: #c53030; box-shadow: 0 4px 15px rgba(197, 48, 48, 0.4); transform: translateY(-2px); }
328
  .product-button i { margin-right: 5px; }
329
- .add-to-cart { background-color: #38a169; } /* Зеленый для корзины */
 
 
330
  .add-to-cart:hover { background-color: #2f855a; box-shadow: 0 4px 15px rgba(47, 133, 90, 0.4); }
331
- #cart-button { position: fixed; bottom: 25px; right: 25px; background-color: #e53e3e; color: white; border: none; border-radius: 50%; width: 55px; height: 55px; font-size: 1.5rem; cursor: pointer; display: none; /* Скрыта по умолчанию */ align-items: center; justify-content: center; box-shadow: 0 4px 15px rgba(229, 62, 62, 0.4); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); z-index: 1000; }
332
- #cart-button .fa-shopping-cart { margin-right: 0; } /* Убрать отступ у иконки */
333
- #cart-button span { position: absolute; top: -5px; right: -5px; background-color: #38a169; color: white; border-radius: 50%; padding: 2px 6px; font-size: 0.7rem; font-weight: bold; }
 
 
334
  .modal { display: none; position: fixed; z-index: 1001; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.6); backdrop-filter: blur(5px); overflow-y: auto; }
335
- .modal-content { background: #fff; margin: 5% auto; padding: 25px; border-radius: 15px; width: 90%; max-width: 700px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); animation: slideIn 0.3s ease-out; position: relative; }
336
- body.dark-mode .modal-content { background: #2d3748; color: #e2e8f0; }
337
  @keyframes slideIn { from { transform: translateY(-30px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
338
  .close { position: absolute; top: 15px; right: 15px; font-size: 1.8rem; color: #aaa; cursor: pointer; transition: color 0.3s; line-height: 1; }
339
  .close:hover { color: #333; }
340
- body.dark-mode .close { color: #718096; }
341
- body.dark-mode .close:hover { color: #cbd5e0; }
342
- .modal-content h2 { margin-top: 0; margin-bottom: 20px; color: #e53e3e; display: flex; align-items: center; gap: 10px;}
343
- body.dark-mode .modal-content h2 { color: #fc8181; }
344
- .cart-item { display: grid; grid-template-columns: auto 1fr auto auto; gap: 15px; align-items: center; padding: 15px 0; border-bottom: 1px solid #eee; }
345
- body.dark-mode .cart-item { border-bottom-color: #4a5568; }
346
  .cart-item:last-child { border-bottom: none; }
347
  .cart-item img { width: 60px; height: 60px; object-fit: contain; border-radius: 8px; background-color: #fff; padding: 5px; grid-column: 1; }
348
  .cart-item-details { grid-column: 2; }
349
  .cart-item-details strong { display: block; margin-bottom: 5px; }
350
- .cart-item-price { font-size: 0.9rem; color: #555; }
351
- body.dark-mode .cart-item-price { color: #a0aec0; }
352
  .cart-item-total { font-weight: bold; text-align: right; grid-column: 3; }
353
- .cart-item-remove { grid-column: 4; background:none; border:none; color:#f56565; cursor:pointer; font-size: 1.3em; padding: 5px; line-height: 1; }
354
  .cart-item-remove:hover { color: #c53030; }
355
- .quantity-input, .color-select { width: 100%; max-width: 180px; padding: 10px; border: 1px solid #e2e8f0; border-radius: 8px; font-size: 1rem; margin: 10px 0; box-sizing: border-box; }
356
- body.dark-mode .quantity-input, body.dark-mode .color-select { background-color: #1a202c; border-color: #4a5568; color: #e2e8f0; }
357
- .cart-summary { margin-top: 20px; text-align: right; border-top: 1px solid #eee; padding-top: 15px; }
358
- body.dark-mode .cart-summary { border-top-color: #4a5568; }
359
  .cart-summary strong { font-size: 1.2rem; }
360
  .cart-actions { margin-top: 25px; display: flex; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
361
  .cart-actions .product-button { width: auto; flex-grow: 1; } /* Кнопки растягиваются */
362
- .clear-cart { background-color: #718096; }
363
- .clear-cart:hover { background-color: #4a5568; box-shadow: 0 4px 15px rgba(74, 85, 104, 0.4); }
364
- .order-button { background-color: #38a169; }
365
  .order-button:hover { background-color: #2f855a; box-shadow: 0 4px 15px rgba(47, 133, 90, 0.4); }
366
- .notification { position: fixed; bottom: 80px; left: 50%; transform: translateX(-50%); background-color: #38a169; color: white; padding: 10px 20px; border-radius: 20px; box-shadow: 0 4px 10px rgba(0,0,0,0.2); z-index: 1002; opacity: 0; transition: opacity 0.5s ease; font-size: 0.9rem;}
 
 
367
  .notification.show { opacity: 1;}
368
- .no-results-message { grid-column: 1 / -1; text-align: center; padding: 40px; font-size: 1.1rem; color: #777; }
369
- body.dark-mode .no-results-message { color: #a0aec0; }
370
  </style>
371
  </head>
372
  <body>
@@ -766,15 +777,17 @@ def catalog():
766
  return;
767
  }
768
  let total = 0;
769
- let orderText = "Новый Заказ от Soola Cosmetics:%0A%0A";
770
  cart.forEach((item, index) => {
771
  const itemTotal = item.price * item.quantity;
772
  total += itemTotal;
773
  const colorText = item.color !== 'N/A' ? ` (Цвет: ${item.color})` : '';
 
774
  orderText += `${index + 1}. ${item.name}${colorText} - ${item.price.toFixed(2)} ${currencyCode} × ${item.quantity} = ${itemTotal.toFixed(2)} ${currencyCode}%0A`;
775
  });
776
- orderText += `%0A*Итого: ${total.toFixed(2)} ${currencyCode}*%0A%0A`;
777
 
 
778
  const userInfo = {{ session.get('user_info', {})|tojson }};
779
  if (userInfo && userInfo.login) {
780
  orderText += `Заказчик: ${userInfo.get('first_name', '')} ${userInfo.get('last_name', '')}%0A`;
@@ -787,13 +800,14 @@ def catalog():
787
 
788
  // Добавляем текущую дату и время
789
  const now = new Date();
790
- const dateTimeString = now.toLocaleString('ru-RU');
791
  orderText += `%0AДата заказа: ${dateTimeString}`;
792
 
793
-
794
- // Номер WhatsApp - убедитесь, что он правильный
795
- const whatsappNumber = "996555360556";
796
- const whatsappUrl = `https://api.whatsapp.com/send?phone=${whatsappNumber}&text=${encodeURIComponent(orderText)}`; // Используем encodeURIComponent для текста
 
797
  window.open(whatsappUrl, '_blank');
798
  }
799
 
@@ -908,9 +922,6 @@ def catalog():
908
  currency_code=CURRENCY_CODE
909
  )
910
 
911
- # --- Остальные маршруты (product_detail, login, auto_login, logout, admin, force_upload, force_download) ---
912
- # --- Код для этих маршрутов остается таким же, как в предыдущем ответе ---
913
- # --- ... (включая LOGIN_TEMPLATE и admin_html) ... ---
914
 
915
  @app.route('/product/<int:index>')
916
  def product_detail(index):
@@ -924,10 +935,11 @@ def product_detail(index):
924
  logging.warning(f"Попытка доступа к несуществующему продукту с индексом {index}")
925
  return "Товар не найден", 404
926
 
 
927
  detail_html = '''
928
  {# Используем Jinja комментарий #}
929
  <div style="padding: 10px;">
930
- <h2 style="font-size: 1.6rem; font-weight: 600; margin-bottom: 15px; text-align: center; color: #e53e3e;">{{ product['name'] }}</h2>
931
  {# Swiper Slider for Photos #}
932
  <div class="swiper-container" style="max-width: 450px; margin: 0 auto 20px; border-radius: 10px; overflow: hidden; background-color: #fff;">
933
  <div class="swiper-wrapper">
@@ -950,8 +962,8 @@ def product_detail(index):
950
  {# Элементы управления Swiper (показываем только если фото больше 1) #}
951
  {% if product.get('photos') and product['photos']|length > 1 %}
952
  <div class="swiper-pagination" style="position: relative; bottom: 5px;"></div>
953
- <div class="swiper-button-next" style="color: #e53e3e;"></div>
954
- <div class="swiper-button-prev" style="color: #e53e3e;"></div>
955
  {% endif %}
956
  </div>
957
 
@@ -959,9 +971,9 @@ def product_detail(index):
959
  <div style="margin-top: 20px; font-size: 1rem; line-height: 1.7;">
960
  <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
961
  {% if is_authenticated %}
962
- <p style="font-size: 1.2rem; font-weight: bold; color: #e53e3e;"><strong>Цена:</strong> {{ "%.2f"|format(product['price']) }} {{ currency_code }}</p>
963
  {% else %}
964
- <p><strong>Цена:</strong> <a href="{{ url_for('login') }}" style="color: #3b82f6; text-decoration: underline;">Доступна после входа</a></p>
965
  {% endif %}
966
  {# Используем safe фильтр для рендеринга <br> тегов из описания #}
967
  <p><strong>Описание:</strong><br> {{ product.get('description', 'Описание отсутствует.')|replace('\n', '<br>')|safe }}</p>
@@ -982,7 +994,7 @@ def product_detail(index):
982
 
983
  # --- Маршруты аутентификации ---
984
 
985
- # Шаблон для страницы входа
986
  LOGIN_TEMPLATE = '''
987
  <!DOCTYPE html>
988
  <html lang="ru">
@@ -992,16 +1004,16 @@ LOGIN_TEMPLATE = '''
992
  <title>Вход - Soola Cosmetics</title>
993
  <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
994
  <style>
995
- body { font-family: 'Poppins', sans-serif; background: linear-gradient(135deg, #fce3e3, #ffebeb); display: flex; justify-content: center; align-items: center; min-height: 100vh; padding: 20px; }
996
  .container { max-width: 400px; width: 100%; background: #fff; padding: 30px 40px; border-radius: 15px; box-shadow: 0 5px 20px rgba(0,0,0,0.1); text-align: center; }
997
- h2 { color: #e53e3e; margin-bottom: 25px; font-weight: 600; }
998
- label { display: block; text-align: left; margin: 15px 0 5px; font-weight: 500; color: #555; }
999
- input[type="text"], input[type="password"] { width: 100%; padding: 12px; margin-bottom: 15px; border: 1px solid #ddd; border-radius: 8px; box-sizing: border-box; font-size: 1rem; }
1000
- input:focus { border-color: #e53e3e; outline: none; box-shadow: 0 0 0 2px rgba(229, 62, 62, 0.2); }
1001
- button { width: 100%; padding: 12px; background-color: #e53e3e; color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 1rem; font-weight: 600; transition: background-color 0.3s ease; margin-top: 10px; }
1002
- button:hover { background-color: #c53030; }
1003
- .error { color: #c53030; background-color: #ffebeb; border: 1px solid #f5c6cb; padding: 10px; border-radius: 8px; margin-bottom: 15px; font-size: 0.9rem; text-align: left;}
1004
- .back-link { display: inline-block; margin-top: 20px; color: #3b82f6; text-decoration: none; font-size: 0.9rem; }
1005
  .back-link:hover { text-decoration: underline; }
1006
  </style>
1007
  </head>
@@ -1125,7 +1137,7 @@ def logout():
1125
  return logout_response_html
1126
 
1127
  # --- Админ-панель ---
1128
- # Шаблон админ-панели (admin_html) остается как в предыдущем ответе
1129
  ADMIN_TEMPLATE = '''
1130
  <!DOCTYPE html>
1131
  <html lang="ru">
@@ -1136,60 +1148,70 @@ ADMIN_TEMPLATE = '''
1136
  <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
1137
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
1138
  <style>
1139
- body { font-family: 'Poppins', sans-serif; background-color: #f4f7f6; color: #333; padding: 20px; line-height: 1.6; }
1140
  .container { max-width: 1200px; margin: 0 auto; background-color: #fff; padding: 25px; border-radius: 10px; box-shadow: 0 3px 10px rgba(0,0,0,0.05); }
1141
- .header { padding-bottom: 15px; margin-bottom: 25px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px;}
1142
- h1, h2, h3 { font-weight: 600; color: #e53e3e; margin-bottom: 15px; }
1143
  h1 { font-size: 1.8rem; }
1144
  h2 { font-size: 1.5rem; margin-top: 30px; display: flex; align-items: center; gap: 8px; }
1145
- h3 { font-size: 1.2rem; color: #c53030; margin-top: 20px; }
1146
- .section { margin-bottom: 30px; padding: 20px; background-color: #fdfdfe; border: 1px solid #eee; border-radius: 8px; }
1147
  form { margin-bottom: 20px; }
1148
- label { font-weight: 500; margin-top: 10px; display: block; color: #555; font-size: 0.9rem;}
1149
- input[type="text"], input[type="number"], input[type="password"], textarea, select { width: 100%; padding: 10px 12px; margin-top: 5px; border: 1px solid #ddd; border-radius: 6px; font-size: 0.95rem; box-sizing: border-box; transition: border-color 0.3s ease; }
1150
- input:focus, textarea:focus, select:focus { border-color: #e53e3e; outline: none; box-shadow: 0 0 0 2px rgba(229, 62, 62, 0.1); }
1151
  textarea { min-height: 80px; resize: vertical; }
1152
- input[type="file"] { padding: 8px; background-color: #f9f9f9; cursor: pointer;}
1153
- input[type="file"]::file-selector-button { padding: 5px 10px; border-radius: 4px; background-color: #eee; border: 1px solid #ccc; cursor: pointer; margin-right: 10px;}
1154
- button, .button { padding: 10px 18px; border: none; border-radius: 6px; background-color: #e53e3e; color: white; font-weight: 500; cursor: pointer; transition: background-color 0.3s ease, transform 0.1s ease; margin-top: 15px; font-size: 0.95rem; display: inline-flex; align-items: center; gap: 5px; text-decoration: none; line-height: 1.2;}
1155
- button:hover, .button:hover { background-color: #c53030; }
1156
  button:active, .button:active { transform: scale(0.98); }
1157
  button[type="submit"] { min-width: 120px; justify-content: center; }
1158
- .delete-button { background-color: #718096; }
1159
- .delete-button:hover { background-color: #4a5568; }
1160
- .add-button { background-color: #38a169; }
1161
- .add-button:hover { background-color: #2f855a; }
1162
  .item-list { display: grid; gap: 20px; }
1163
- .item { background: #fff; padding: 15px 20px; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.07); border: 1px solid #eee; }
1164
- .item p { margin: 5px 0; font-size: 0.9rem; color: #444; }
1165
- .item strong { color: #333; }
1166
- .item .description { font-size: 0.85rem; color: #666; max-height: 60px; overflow: hidden; text-overflow: ellipsis; }
1167
  .item-actions { margin-top: 15px; display: flex; gap: 10px; flex-wrap: wrap; }
1168
- .edit-form-container { margin-top: 15px; padding: 20px; background: #f9fafb; border: 1px dashed #ddd; border-radius: 6px; display: none; /* Скрыто по умолчанию */ }
1169
- details { background-color: #fdfdfe; border: 1px solid #eee; border-radius: 8px; margin-bottom: 20px; }
1170
- details > summary { cursor: pointer; font-weight: 600; color: #4a5568; display: block; padding: 15px; border-bottom: 1px solid #eee; list-style: none; /* Убрать стандартный маркер */ position: relative; }
1171
- details > summary::after { content: '\\f078'; /* FontAwesome chevron-down */ font-family: 'Font Awesome 6 Free'; font-weight: 900; position: absolute; right: 20px; top: 50%; transform: translateY(-50%); transition: transform 0.2s ease; }
 
 
 
1172
  details[open] > summary::after { transform: translateY(-50%) rotate(180deg); }
1173
- details[open] > summary { border-bottom: 1px solid #eee; }
1174
  details .form-content { padding: 20px; }
1175
  .color-input-group { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
1176
  .color-input-group input { flex-grow: 1; margin: 0; }
1177
- .remove-color-btn { background-color: #f56565; padding: 6px 10px; font-size: 0.8rem; margin-top: 0; line-height: 1; }
1178
  .remove-color-btn:hover { background-color: #e53e3e; }
1179
- .photo-preview img { max-width: 70px; max-height: 70px; border-radius: 5px; margin: 5px 5px 0 0; border: 1px solid #eee; object-fit: cover;}
 
 
 
1180
  .sync-buttons { display: flex; gap: 10px; margin-bottom: 20px; flex-wrap: wrap; }
 
 
 
1181
  .flex-container { display: flex; flex-wrap: wrap; gap: 20px; }
1182
  .flex-item { flex: 1; min-width: 350px; /* Минимальная ширина колонки */ }
1183
  .message { padding: 10px 15px; border-radius: 6px; margin-bottom: 15px; font-size: 0.9rem;}
1184
- .message.success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb;}
1185
- .message.error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb;}
 
1186
  </style>
1187
  </head>
1188
  <body>
1189
  <div class="container">
1190
  <div class="header">
1191
  <h1><i class="fas fa-tools"></i> Админ-панель Soola Cosmetics</h1>
1192
- <a href="{{ url_for('catalog') }}" class="button" style="background-color: #3b82f6;"><i class="fas fa-store"></i> Перейти в каталог</a>
1193
  </div>
1194
 
1195
  {# Сообщения об успехе/ошибке #}
@@ -1205,13 +1227,13 @@ ADMIN_TEMPLATE = '''
1205
  <h2><i class="fas fa-sync-alt"></i> Синхронизация с Hugging Face</h2>
1206
  <div class="sync-buttons">
1207
  <form method="POST" action="{{ url_for('force_upload') }}" style="display: inline;" onsubmit="return confirm('Вы уверены, что хотите принудительно загрузить локальные данные на сервер? Это перезапишет данные на сервере.');">
1208
- <button type="submit" class="button" title="Загрузить локальные файлы на Hugging Face"><i class="fas fa-upload"></i> Загрузить на HF</button>
1209
  </form>
1210
  <form method="POST" action="{{ url_for('force_download') }}" style="display: inline;" onsubmit="return confirm('Вы уверены, что хотите принудительно скачать данные с сервера? Это перезапишет ваши локальные файлы.');">
1211
- <button type="submit" class="button delete-button" title="Скачать файлы с Hugging Face (перезапишет локальные)"><i class="fas fa-download"></i> Скачать с HF</button>
1212
  </form>
1213
  </div>
1214
- <p style="font-size: 0.85rem; color: #666;">Резервное копирование на Hugging Face происходит автоматически каждые 30 минут, а также после каждого сохранения данных. Используйте эти кнопки для немедленной синхронизации.</p>
1215
  </div>
1216
 
1217
 
@@ -1226,7 +1248,7 @@ ADMIN_TEMPLATE = '''
1226
  <input type="hidden" name="action" value="add_category">
1227
  <label for="add_category_name">Название новой категории:</label>
1228
  <input type="text" id="add_category_name" name="category_name" required>
1229
- <button type="submit" class="add-button"><i class="fas fa-plus"></i> Добавить</button>
1230
  </form>
1231
  </div>
1232
  </details>
@@ -1240,7 +1262,7 @@ ADMIN_TEMPLATE = '''
1240
  <form method="POST" style="margin: 0;" onsubmit="return confirm('Вы уверены, что хотите удалить категорию \'{{ category }}\'? Товары этой категории будут помечены как \'Без категории\'.');">
1241
  <input type="hidden" name="action" value="delete_category">
1242
  <input type="hidden" name="category_name" value="{{ category }}">
1243
- <button type="submit" class="delete-button" style="padding: 5px 10px; font-size: 0.8rem; margin: 0;"><i class="fas fa-trash-alt"></i></button>
1244
  </form>
1245
  </div>
1246
  {% endfor %}
@@ -1272,7 +1294,7 @@ ADMIN_TEMPLATE = '''
1272
  <input type="text" id="country" name="country">
1273
  <label for="city">Город:</label>
1274
  <input type="text" id="city" name="city">
1275
- <button type="submit" class="add-button"><i class="fas fa-save"></i> Сохранить пользователя</button>
1276
  </form>
1277
  </div>
1278
  </details>
@@ -1289,7 +1311,7 @@ ADMIN_TEMPLATE = '''
1289
  <form method="POST" style="margin: 0;" onsubmit="return confirm('Вы уверены, что хотите удалить пользователя \'{{ login }}\'?');">
1290
  <input type="hidden" name="action" value="delete_user">
1291
  <input type="hidden" name="login" value="{{ login }}">
1292
- <button type="submit" class="delete-button"><i class="fas fa-user-slash"></i> Удалить</button>
1293
  </form>
1294
  {# Можно добавить кнопку редактирования пользователя, если нужно #}
1295
  </div>
@@ -1330,12 +1352,12 @@ ADMIN_TEMPLATE = '''
1330
  <div id="add-color-inputs">
1331
  <div class="color-input-group">
1332
  <input type="text" name="colors" placeholder="Например: Розовый">
1333
- <button type="button" class="remove-color-btn" onclick="removeColorInput(this)"><i class="fas fa-times"></i></button>
1334
  </div>
1335
  </div>
1336
- <button type="button" class="button add-button" style="margin-top: 5px; background-color: #63b3ed;" onclick="addColorInput('add-color-inputs')"><i class="fas fa-palette"></i> Добавить поле для цвета/варианта</button>
1337
  <br>
1338
- <button type="submit" class="add-button" style="margin-top: 20px;"><i class="fas fa-save"></i> Добавить товар</button>
1339
  </form>
1340
  </div>
1341
  </details>
@@ -1358,24 +1380,24 @@ ADMIN_TEMPLATE = '''
1358
  </div>
1359
  {# Информация о товаре #}
1360
  <div style="flex-grow: 1;">
1361
- <h3 style="margin-top: 0; margin-bottom: 5px; color: #333;">{{ product['name'] }}</h3>
1362
  <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
1363
  <p><strong>Цена:</strong> {{ "%.2f"|format(product['price']) }} {{ currency_code }}</p>
1364
  <p class="description" title="{{ product.get('description', '') }}"><strong>Описание:</strong> {{ product.get('description', 'N/A')[:150] }}{% if product.get('description', '')|length > 150 %}...{% endif %}</p>
1365
  {% set colors = product.get('colors', []) %}
1366
  <p><strong>Цвета/Вар-ты:</strong> {{ colors|select('ne', '')|join(', ') if colors|select('ne', '')|list|length > 0 else 'Нет' }}</p>
1367
  {% if product.get('photos') and product['photos']|length > 1 %}
1368
- <p style="font-size: 0.8rem; color: #666;">(Всего фото: {{ product['photos']|length }})</p>
1369
  {% endif %}
1370
  </div>
1371
  </div>
1372
 
1373
  <div class="item-actions">
1374
- <button type="button" class="button" onclick="toggleEditForm('edit-form-{{ loop.index0 }}')"><i class="fas fa-edit"></i> Редактировать</button>
1375
  <form method="POST" style="margin:0;" onsubmit="return confirm('Вы уверены, что хотите удалить товар \'{{ product['name'] }}\'?');">
1376
  <input type="hidden" name="action" value="delete_product">
1377
  <input type="hidden" name="index" value="{{ loop.index0 }}">
1378
- <button type="submit" class="delete-button"><i class="fas fa-trash-alt"></i> Удалить</button>
1379
  </form>
1380
  </div>
1381
 
@@ -1416,7 +1438,7 @@ ADMIN_TEMPLATE = '''
1416
  {% if color.strip() %} {# Отображаем только не пустые #}
1417
  <div class="color-input-group">
1418
  <input type="text" name="colors" value="{{ color }}">
1419
- <button type="button" class="remove-color-btn" onclick="removeColorInput(this)"><i class="fas fa-times"></i></button>
1420
  </div>
1421
  {% endif %}
1422
  {% endfor %}
@@ -1424,13 +1446,13 @@ ADMIN_TEMPLATE = '''
1424
  {# Добавляем одно пустое поле, если цветов нет #}
1425
  <div class="color-input-group">
1426
  <input type="text" name="colors" placeholder="Например: Красный">
1427
- <button type="button" class="remove-color-btn" onclick="removeColorInput(this)"><i class="fas fa-times"></i></button>
1428
  </div>
1429
  {% endif %}
1430
  </div>
1431
- <button type="button" class="button add-button" style="margin-top: 5px; background-color: #63b3ed;" onclick="addColorInput('edit-color-inputs-{{ loop.index0 }}')"><i class="fas fa-palette"></i> Добавить поле для цвета</button>
1432
  <br>
1433
- <button type="submit" class="add-button" style="margin-top: 20px;"><i class="fas fa-save"></i> Сохранить изменения</button>
1434
  </form>
1435
  </div>
1436
  </div>
@@ -1456,10 +1478,10 @@ ADMIN_TEMPLATE = '''
1456
  if (container) {
1457
  const newInputGroup = document.createElement('div');
1458
  newInputGroup.className = 'color-input-group';
1459
- newInputGroup.innerHTML = \`
1460
  <input type="text" name="colors" placeholder="Новый цвет/вариант">
1461
  <button type="button" class="remove-color-btn" onclick="removeColorInput(this)"><i class="fas fa-times"></i></button>
1462
- \`;
1463
  container.appendChild(newInputGroup);
1464
  // Установить фокус на новый инпут
1465
  const newInput = newInputGroup.querySelector('input[name="colors"]');
@@ -1493,7 +1515,11 @@ def admin():
1493
  """Админ-панель для управления товарами, категориями и пользователями."""
1494
  # Здесь должна быть проверка прав администратора!
1495
  # Пример: if session.get('user') != 'admin_login': return "Доступ запрещен", 403
1496
- # Для простоты пока опускаем
 
 
 
 
1497
 
1498
  data = load_data()
1499
  products = data.get('products', [])
@@ -1594,6 +1620,13 @@ def admin():
1594
  flash(f"Ошибка при загрузке фото {photo.filename}.", 'error')
1595
  elif photo and not photo.filename:
1596
  logging.warning("Получен пустой объект файла фото при добавлении товара.")
 
 
 
 
 
 
 
1597
 
1598
  new_product = {
1599
  'name': name, 'price': price, 'description': description,
@@ -1670,11 +1703,34 @@ def admin():
1670
  except Exception as e:
1671
  logging.error(f"Ошибка загрузки нового фото {photo.filename}: {e}", exc_info=True)
1672
  flash(f"Ошибка при загрузке нового фото {photo.filename}.", 'error')
 
 
 
 
 
 
1673
 
1674
  # Если были успешно загружены новые фото, заменяем старый список
1675
  if new_photos_list:
1676
  logging.info(f"Список фото для товара {product_to_edit['name']} обновлен.")
1677
  # TODO: Удалить старые фото с HF? Это сложнее, требует хранения списка старых фото.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1678
  product_to_edit['photos'] = new_photos_list
1679
  flash("Фотографии товара успешно обновлены.", "success")
1680
  elif uploaded_count == 0 and any(f.filename for f in photos_files):
@@ -1696,9 +1752,28 @@ def admin():
1696
  index = int(index_str)
1697
  if not (0 <= index < len(products)): raise IndexError("Индекс вне диапазона")
1698
  deleted_product = products.pop(index)
1699
- # TODO: Удалить фото с HF?
1700
- save_data(data)
1701
  product_name = deleted_product.get('name', 'N/A')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1702
  logging.info(f"Товар '{product_name}' (индекс {index}) удален.")
1703
  flash(f"Товар '{product_name}' удален.", 'success')
1704
  except (ValueError, IndexError):
@@ -1774,12 +1849,14 @@ def admin():
1774
  )
1775
 
1776
  # --- Маршруты для принудительной синхронизации ---
1777
- # Добавим flash сообщения
1778
- from flask import flash # Убедитесь, что flash импортирован
1779
 
1780
  @app.route('/force_upload', methods=['POST'])
1781
  def force_upload():
1782
- # Добавить проверку прав администратора
 
 
 
 
1783
  logging.info("Запущена принудительная загрузка данных на Hugging Face...")
1784
  try:
1785
  upload_db_to_hf()
@@ -1791,7 +1868,11 @@ def force_upload():
1791
 
1792
  @app.route('/force_download', methods=['POST'])
1793
  def force_download():
1794
- # Добавить проверку прав администратора
 
 
 
 
1795
  logging.info("Запущено принудительное скачивание данных с Hugging Face...")
1796
  try:
1797
  download_db_from_hf()
@@ -1823,6 +1904,7 @@ if __name__ == '__main__':
1823
  # debug=False для продакшена! Установите в True только для локальной разработки.
1824
  # Использование app.run() подходит только для разработки.
1825
  # Для продакшена используйте WSGI сервер, например, Gunicorn или Waitress.
1826
- # Пример с Gunicorn: gunicorn --bind 0.0.0.0:7860 your_app_file:app
1827
- # Пример с Waitress: waitress-serve --host 0.0.0.0 --port 7860 your_app_file:app
1828
  app.run(debug=False, host='0.0.0.0', port=port)
 
 
1
 
2
+
3
+ from flask import Flask, render_template_string, request, redirect, url_for, session, send_file, flash
4
  import json
5
  import os
6
  import logging
 
277
  <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
278
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.css">
279
  <style>
280
+ /* Общие стили */
281
  * { margin: 0; padding: 0; box-sizing: border-box; }
282
+ body { font-family: 'Poppins', sans-serif; background: #f0f9f4; color: #2d332f; line-height: 1.6; transition: background 0.3s, color 0.3s; }
283
+ body.dark-mode { background: #1a2b26; color: #c8d8d3; }
284
  .container { max-width: 1300px; margin: 0 auto; padding: 20px; }
285
+ .header { display: flex; justify-content: space-between; align-items: center; padding: 15px 0; border-bottom: 1px solid #d1e7dd; }
286
+ body.dark-mode .header { border-bottom-color: #2c4a41; }
287
+ .header h1 { font-size: 1.8rem; font-weight: 600; color: #1C6758; } /* Логотип Soola - Темно-зеленый */
288
  .auth-links { display: flex; gap: 15px; align-items: center; }
289
+ .auth-links a { color: #3D8361; text-decoration: none; font-weight: 500; } /* Ссылки - Средне-зеленый */
290
  .auth-links a:hover { text-decoration: underline; }
291
+ body.dark-mode .auth-links a { color: #55a683; } /* Ссылки в темной теме */
292
  .auth-links span { font-weight: 500; }
293
+ body.dark-mode .auth-links span { color: #b0c8c1;}
294
+ .theme-toggle { background: none; border: none; font-size: 1.5rem; cursor: pointer; color: #7a8d85; transition: color 0.3s ease; }
295
+ .theme-toggle:hover { color: #3D8361; }
296
+ body.dark-mode .theme-toggle { color: #8aa39a; }
297
+ body.dark-mode .theme-toggle:hover { color: #55a683; }
298
+ .store-address { padding: 15px; text-align: center; background-color: #ffffff; margin: 20px 0; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); font-size: 1rem; color: #44524c; }
299
+ body.dark-mode .store-address { background-color: #253f37; color: #b0c8c1; }
300
+
301
+ /* Фильтры и поиск */
302
  .filters-container { margin: 20px 0; display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }
303
  .search-container { margin: 20px 0; text-align: center; }
304
+ #search-input { width: 90%; max-width: 600px; padding: 12px 18px; font-size: 1rem; border: 1px solid #d1e7dd; border-radius: 25px; outline: none; box-shadow: 0 2px 5px rgba(0,0,0,0.05); transition: all 0.3s ease; }
305
+ body.dark-mode #search-input { background-color: #253f37; border-color: #2c4a41; color: #c8d8d3; }
306
+ #search-input:focus { border-color: #1C6758; box-shadow: 0 0 0 3px rgba(28, 103, 88, 0.2); } /* Фокус - темно-зеленый */
307
+ body.dark-mode #search-input:focus { border-color: #3D8361; box-shadow: 0 0 0 3px rgba(61, 131, 97, 0.3); } /* Фокус в темной теме */
308
+ .category-filter { padding: 8px 16px; border: 1px solid #d1e7dd; border-radius: 20px; background-color: #fff; cursor: pointer; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); font-size: 0.9rem; font-weight: 400; color: #1C6758; } /* Цвет текста кнопки фильтра */
309
+ body.dark-mode .category-filter { background-color: #253f37; border-color: #2c4a41; color: #97b7ae; }
310
+ .category-filter.active, .category-filter:hover { background-color: #1C6758; color: white; border-color: #1C6758; box-shadow: 0 2px 10px rgba(28, 103, 88, 0.3); } /* Активный/ховер фильтр - темно-зеленый */
311
+ body.dark-mode .category-filter.active, body.dark-mode .category-filter:hover { background-color: #3D8361; border-color: #3D8361; color: #1a2b26; box-shadow: 0 2px 10px rgba(61, 131, 97, 0.4); } /* Активный/ховер в темной теме */
312
+
313
+ /* Сетка товаров */
314
  .products-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 20px; padding: 10px; }
315
  @media (min-width: 600px) { .products-grid { grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); } }
316
+ .product { background: #fff; border-radius: 15px; padding: 0; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08); transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.3s ease; overflow: hidden; display: flex; flex-direction: column; justify-content: space-between; height: 100%; border: 1px solid #e1f0e9;}
317
+ body.dark-mode .product { background: #253f37; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); border-color: #2c4a41; }
318
  .product:hover { transform: translateY(-5px) scale(1.02); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12); }
319
  body.dark-mode .product:hover { box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3); }
320
+ .product-image { width: 100%; aspect-ratio: 1 / 1; background-color: #fff; border-radius: 10px 10px 0 0; overflow: hidden; display: flex; justify-content: center; align-items: center; margin-bottom: 0; }
321
  .product-image img { max-width: 100%; max-height: 100%; object-fit: contain; transition: transform 0.3s ease; }
322
  .product-image img:hover { transform: scale(1.08); }
323
+ .product-info { padding: 15px; flex-grow: 1; display: flex; flex-direction: column; justify-content: center; }
324
+ .product h2 { font-size: 1.1rem; font-weight: 600; margin: 0 0 8px 0; text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: #2d332f; }
325
+ body.dark-mode .product h2 { color: #c8d8d3; }
326
+ .product-price { font-size: 1.2rem; color: #1C6758; font-weight: 700; text-align: center; margin: 5px 0; } /* Цена - темно-зеленая */
327
+ body.dark-mode .product-price { color: #55a683; } /* Цена в темной теме */
328
+ .product-description { font-size: 0.85rem; color: #7a8d85; text-align: center; margin-bottom: 15px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
329
+ body.dark-mode .product-description { color: #8aa39a; }
330
+ .product-actions { padding: 0 15px 15px 15px; display: flex; flex-direction: column; gap: 8px; }
331
+ .product-button { display: block; width: 100%; padding: 10px; border: none; border-radius: 8px; background-color: #1C6758; color: white; font-size: 0.9rem; font-weight: 500; cursor: pointer; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); text-align: center; text-decoration: none; } /* Кнопка - темно-зеленая */
332
+ .product-button:hover { background-color: #164B41; box-shadow: 0 4px 15px rgba(22, 75, 65, 0.4); transform: translateY(-2px); } /* Ховер кнопки - еще темнее зеленый */
333
  .product-button i { margin-right: 5px; }
334
+
335
+ /* Стили корзины */
336
+ .add-to-cart { background-color: #38a169; } /* Зеленый для корзины (можно оставить) */
337
  .add-to-cart:hover { background-color: #2f855a; box-shadow: 0 4px 15px rgba(47, 133, 90, 0.4); }
338
+ #cart-button { position: fixed; bottom: 25px; right: 25px; background-color: #1C6758; color: white; border: none; border-radius: 50%; width: 55px; height: 55px; font-size: 1.5rem; cursor: pointer; display: none; align-items: center; justify-content: center; box-shadow: 0 4px 15px rgba(28, 103, 88, 0.4); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); z-index: 1000; } /* Плавающая кнопка - темно-зеленая */
339
+ #cart-button .fa-shopping-cart { margin-right: 0; }
340
+ #cart-button span { position: absolute; top: -5px; right: -5px; background-color: #38a169; color: white; border-radius: 50%; padding: 2px 6px; font-size: 0.7rem; font-weight: bold; } /* Счетчик остается зеленым */
341
+
342
+ /* Модальные окна */
343
  .modal { display: none; position: fixed; z-index: 1001; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.6); backdrop-filter: blur(5px); overflow-y: auto; }
344
+ .modal-content { background: #f8fcfb; margin: 5% auto; padding: 25px; border-radius: 15px; width: 90%; max-width: 700px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); animation: slideIn 0.3s ease-out; position: relative; }
345
+ body.dark-mode .modal-content { background: #253f37; color: #c8d8d3; }
346
  @keyframes slideIn { from { transform: translateY(-30px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
347
  .close { position: absolute; top: 15px; right: 15px; font-size: 1.8rem; color: #aaa; cursor: pointer; transition: color 0.3s; line-height: 1; }
348
  .close:hover { color: #333; }
349
+ body.dark-mode .close { color: #7a8d85; }
350
+ body.dark-mode .close:hover { color: #b0c8c1; }
351
+ .modal-content h2 { margin-top: 0; margin-bottom: 20px; color: #1C6758; display: flex; align-items: center; gap: 10px;} /* Заголовок модалки - темно-зеленый */
352
+ body.dark-mode .modal-content h2 { color: #55a683; } /* Заголовок модалки в темной теме */
353
+ .cart-item { display: grid; grid-template-columns: auto 1fr auto auto; gap: 15px; align-items: center; padding: 15px 0; border-bottom: 1px solid #d1e7dd; }
354
+ body.dark-mode .cart-item { border-bottom-color: #2c4a41; }
355
  .cart-item:last-child { border-bottom: none; }
356
  .cart-item img { width: 60px; height: 60px; object-fit: contain; border-radius: 8px; background-color: #fff; padding: 5px; grid-column: 1; }
357
  .cart-item-details { grid-column: 2; }
358
  .cart-item-details strong { display: block; margin-bottom: 5px; }
359
+ .cart-item-price { font-size: 0.9rem; color: #44524c; }
360
+ body.dark-mode .cart-item-price { color: #8aa39a; }
361
  .cart-item-total { font-weight: bold; text-align: right; grid-column: 3; }
362
+ .cart-item-remove { grid-column: 4; background:none; border:none; color:#f56565; cursor:pointer; font-size: 1.3em; padding: 5px; line-height: 1; } /* Удаление - красный */
363
  .cart-item-remove:hover { color: #c53030; }
364
+ .quantity-input, .color-select { width: 100%; max-width: 180px; padding: 10px; border: 1px solid #d1e7dd; border-radius: 8px; font-size: 1rem; margin: 10px 0; box-sizing: border-box; }
365
+ body.dark-mode .quantity-input, body.dark-mode .color-select { background-color: #1a2b26; border-color: #2c4a41; color: #c8d8d3; }
366
+ .cart-summary { margin-top: 20px; text-align: right; border-top: 1px solid #d1e7dd; padding-top: 15px; }
367
+ body.dark-mode .cart-summary { border-top-color: #2c4a41; }
368
  .cart-summary strong { font-size: 1.2rem; }
369
  .cart-actions { margin-top: 25px; display: flex; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
370
  .cart-actions .product-button { width: auto; flex-grow: 1; } /* Кнопки растягиваются */
371
+ .clear-cart { background-color: #7a8d85; } /* Кнопка очистки - серо-зеленый */
372
+ .clear-cart:hover { background-color: #5e6e68; box-shadow: 0 4px 15px rgba(94, 110, 104, 0.4); }
373
+ .order-button { background-color: #38a169; } /* Кнопка заказа остается ярко-зеленой */
374
  .order-button:hover { background-color: #2f855a; box-shadow: 0 4px 15px rgba(47, 133, 90, 0.4); }
375
+
376
+ /* Уведомления и сообщения */
377
+ .notification { position: fixed; bottom: 80px; left: 50%; transform: translateX(-50%); background-color: #38a169; color: white; padding: 10px 20px; border-radius: 20px; box-shadow: 0 4px 10px rgba(0,0,0,0.2); z-index: 1002; opacity: 0; transition: opacity 0.5s ease; font-size: 0.9rem;} /* Ув��домление - ярко-зеленое */
378
  .notification.show { opacity: 1;}
379
+ .no-results-message { grid-column: 1 / -1; text-align: center; padding: 40px; font-size: 1.1rem; color: #5e6e68; }
380
+ body.dark-mode .no-results-message { color: #8aa39a; }
381
  </style>
382
  </head>
383
  <body>
 
777
  return;
778
  }
779
  let total = 0;
780
+ let orderText = "Новый Заказ от Soola Cosmetics:%0A%0A"; // Заголовок
781
  cart.forEach((item, index) => {
782
  const itemTotal = item.price * item.quantity;
783
  total += itemTotal;
784
  const colorText = item.color !== 'N/A' ? ` (Цвет: ${item.color})` : '';
785
+ // Форматирование строки заказа
786
  orderText += `${index + 1}. ${item.name}${colorText} - ${item.price.toFixed(2)} ${currencyCode} × ${item.quantity} = ${itemTotal.toFixed(2)} ${currencyCode}%0A`;
787
  });
788
+ orderText += `%0A*Итого: ${total.toFixed(2)} ${currencyCode}*%0A%0A`; // Итоговая сумма
789
 
790
+ // Информация о пользователе, если авторизован
791
  const userInfo = {{ session.get('user_info', {})|tojson }};
792
  if (userInfo && userInfo.login) {
793
  orderText += `Заказчик: ${userInfo.get('first_name', '')} ${userInfo.get('last_name', '')}%0A`;
 
800
 
801
  // Добавляем текущую дату и время
802
  const now = new Date();
803
+ const dateTimeString = now.toLocaleString('ru-RU'); // Формат даты/времени
804
  orderText += `%0AДата заказа: ${dateTimeString}`;
805
 
806
+ // *** ИЗМЕНЕНО: Новый номер WhatsApp ***
807
+ const whatsappNumber = "996997703090"; // Указываем номер без '+' и пробелов
808
+ // Формируем URL для WhatsApp API
809
+ const whatsappUrl = `https://api.whatsapp.com/send?phone=${whatsappNumber}&text=${encodeURIComponent(orderText)}`;
810
+ // Открываем WhatsApp в новой вкладке
811
  window.open(whatsappUrl, '_blank');
812
  }
813
 
 
922
  currency_code=CURRENCY_CODE
923
  )
924
 
 
 
 
925
 
926
  @app.route('/product/<int:index>')
927
  def product_detail(index):
 
935
  logging.warning(f"Попытка доступа к несуществующему продукту с индексом {index}")
936
  return "Товар не найден", 404
937
 
938
+ # HTML для деталей продукта с темно-зелеными акцентами
939
  detail_html = '''
940
  {# Используем Jinja комментарий #}
941
  <div style="padding: 10px;">
942
+ <h2 style="font-size: 1.6rem; font-weight: 600; margin-bottom: 15px; text-align: center; color: #1C6758;">{{ product['name'] }}</h2> {# Темно-зеленый заголовок #}
943
  {# Swiper Slider for Photos #}
944
  <div class="swiper-container" style="max-width: 450px; margin: 0 auto 20px; border-radius: 10px; overflow: hidden; background-color: #fff;">
945
  <div class="swiper-wrapper">
 
962
  {# Элементы управления Swiper (показываем только если фото больше 1) #}
963
  {% if product.get('photos') and product['photos']|length > 1 %}
964
  <div class="swiper-pagination" style="position: relative; bottom: 5px;"></div>
965
+ <div class="swiper-button-next" style="color: #1C6758;"></div> {# Темно-зеленые стрелки #}
966
+ <div class="swiper-button-prev" style="color: #1C6758;"></div> {# Темно-зеленые стрелки #}
967
  {% endif %}
968
  </div>
969
 
 
971
  <div style="margin-top: 20px; font-size: 1rem; line-height: 1.7;">
972
  <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
973
  {% if is_authenticated %}
974
+ <p style="font-size: 1.2rem; font-weight: bold; color: #1C6758;"><strong>Цена:</strong> {{ "%.2f"|format(product['price']) }} {{ currency_code }}</p> {# Темно-зеленая цена #}
975
  {% else %}
976
+ <p><strong>Цена:</strong> <a href="{{ url_for('login') }}" style="color: #3D8361; text-decoration: underline;">Доступна после входа</a></p> {# Средне-зеленая ссылка #}
977
  {% endif %}
978
  {# Используем safe фильтр для рендеринга <br> тегов из описания #}
979
  <p><strong>Описание:</strong><br> {{ product.get('description', 'Описание отсутствует.')|replace('\n', '<br>')|safe }}</p>
 
994
 
995
  # --- Маршруты аутентификации ---
996
 
997
+ # Шаблон для страницы входа с темно-зелеными акцентами
998
  LOGIN_TEMPLATE = '''
999
  <!DOCTYPE html>
1000
  <html lang="ru">
 
1004
  <title>Вход - Soola Cosmetics</title>
1005
  <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
1006
  <style>
1007
+ body { font-family: 'Poppins', sans-serif; background: linear-gradient(135deg, #d1e7dd, #e9f5f0); display: flex; justify-content: center; align-items: center; min-height: 100vh; padding: 20px; } /* Светло-зеленый градиент */
1008
  .container { max-width: 400px; width: 100%; background: #fff; padding: 30px 40px; border-radius: 15px; box-shadow: 0 5px 20px rgba(0,0,0,0.1); text-align: center; }
1009
+ h2 { color: #1C6758; margin-bottom: 25px; font-weight: 600; } /* Темно-зеленый заголовок */
1010
+ label { display: block; text-align: left; margin: 15px 0 5px; font-weight: 500; color: #44524c; }
1011
+ input[type="text"], input[type="password"] { width: 100%; padding: 12px; margin-bottom: 15px; border: 1px solid #c4d9d1; border-radius: 8px; box-sizing: border-box; font-size: 1rem; }
1012
+ input:focus { border-color: #1C6758; outline: none; box-shadow: 0 0 0 2px rgba(28, 103, 88, 0.2); } /* Фокус - темно-зеленый */
1013
+ button { width: 100%; padding: 12px; background-color: #1C6758; color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 1rem; font-weight: 600; transition: background-color 0.3s ease; margin-top: 10px; } /* Кнопка - темно-зеленая */
1014
+ button:hover { background-color: #164B41; } /* Ховер кнопки - темнее */
1015
+ .error { color: #721c24; background-color: #f8d7da; border: 1px solid #f5c6cb; padding: 10px; border-radius: 8px; margin-bottom: 15px; font-size: 0.9rem; text-align: left;} /* Ошибка - красный */
1016
+ .back-link { display: inline-block; margin-top: 20px; color: #3D8361; text-decoration: none; font-size: 0.9rem; } /* Ссылка назад - средне-зеленая */
1017
  .back-link:hover { text-decoration: underline; }
1018
  </style>
1019
  </head>
 
1137
  return logout_response_html
1138
 
1139
  # --- Админ-панель ---
1140
+ # Шаблон админ-панели с темно-зелеными акцентами
1141
  ADMIN_TEMPLATE = '''
1142
  <!DOCTYPE html>
1143
  <html lang="ru">
 
1148
  <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
1149
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
1150
  <style>
1151
+ body { font-family: 'Poppins', sans-serif; background-color: #e9f5f0; color: #2d332f; padding: 20px; line-height: 1.6; } /* Светло-зеленый фон */
1152
  .container { max-width: 1200px; margin: 0 auto; background-color: #fff; padding: 25px; border-radius: 10px; box-shadow: 0 3px 10px rgba(0,0,0,0.05); }
1153
+ .header { padding-bottom: 15px; margin-bottom: 25px; border-bottom: 1px solid #d1e7dd; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px;}
1154
+ h1, h2, h3 { font-weight: 600; color: #1C6758; margin-bottom: 15px; } /* Темно-зеленые заголовки */
1155
  h1 { font-size: 1.8rem; }
1156
  h2 { font-size: 1.5rem; margin-top: 30px; display: flex; align-items: center; gap: 8px; }
1157
+ h3 { font-size: 1.2rem; color: #164B41; margin-top: 20px; } /* Темнее зеленый для подзаголовков */
1158
+ .section { margin-bottom: 30px; padding: 20px; background-color: #f8fcfb; border: 1px solid #d1e7dd; border-radius: 8px; }
1159
  form { margin-bottom: 20px; }
1160
+ label { font-weight: 500; margin-top: 10px; display: block; color: #44524c; font-size: 0.9rem;}
1161
+ input[type="text"], input[type="number"], input[type="password"], textarea, select { width: 100%; padding: 10px 12px; margin-top: 5px; border: 1px solid #c4d9d1; border-radius: 6px; font-size: 0.95rem; box-sizing: border-box; transition: border-color 0.3s ease; }
1162
+ input:focus, textarea:focus, select:focus { border-color: #1C6758; outline: none; box-shadow: 0 0 0 2px rgba(28, 103, 88, 0.1); } /* Фокус - темно-зеленый */
1163
  textarea { min-height: 80px; resize: vertical; }
1164
+ input[type="file"] { padding: 8px; background-color: #f0f9f4; cursor: pointer; border: 1px solid #c4d9d1;}
1165
+ input[type="file"]::file-selector-button { padding: 5px 10px; border-radius: 4px; background-color: #e0f0e9; border: 1px solid #c4d9d1; cursor: pointer; margin-right: 10px;}
1166
+ button, .button { padding: 10px 18px; border: none; border-radius: 6px; background-color: #1C6758; color: white; font-weight: 500; cursor: pointer; transition: background-color 0.3s ease, transform 0.1s ease; margin-top: 15px; font-size: 0.95rem; display: inline-flex; align-items: center; gap: 5px; text-decoration: none; line-height: 1.2;} /* Кнопка - темно-зеленая */
1167
+ button:hover, .button:hover { background-color: #164B41; } /* Ховер кнопки - темнее */
1168
  button:active, .button:active { transform: scale(0.98); }
1169
  button[type="submit"] { min-width: 120px; justify-content: center; }
1170
+ .delete-button { background-color: #f56565; } /* Удаление - красный */
1171
+ .delete-button:hover { background-color: #e53e3e; }
1172
+ .add-button { background-color: #38a169; } /* Добавить - ярко-зеленый */
1173
+ .add-button:hover { background-color: #2f855a; }
1174
  .item-list { display: grid; gap: 20px; }
1175
+ .item { background: #fff; padding: 15px 20px; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.07); border: 1px solid #e1f0e9; }
1176
+ .item p { margin: 5px 0; font-size: 0.9rem; color: #44524c; }
1177
+ .item strong { color: #2d332f; }
1178
+ .item .description { font-size: 0.85rem; color: #5e6e68; max-height: 60px; overflow: hidden; text-overflow: ellipsis; }
1179
  .item-actions { margin-top: 15px; display: flex; gap: 10px; flex-wrap: wrap; }
1180
+ /* Кнопка редактирования - основной темно-зеленый */
1181
+ .item-actions button:not(.delete-button) { background-color: #1C6758; }
1182
+ .item-actions button:not(.delete-button):hover { background-color: #164B41; }
1183
+ .edit-form-container { margin-top: 15px; padding: 20px; background: #f0f9f4; border: 1px dashed #c4d9d1; border-radius: 6px; display: none; /* Скрыто по умолчанию */ }
1184
+ details { background-color: #f8fcfb; border: 1px solid #d1e7dd; border-radius: 8px; margin-bottom: 20px; }
1185
+ details > summary { cursor: pointer; font-weight: 600; color: #164B41; display: block; padding: 15px; border-bottom: 1px solid #d1e7dd; list-style: none; /* Убрать стандартный маркер */ position: relative; } /* Заголовок details - темнее зеленый */
1186
+ details > summary::after { content: '\\f078'; /* FontAwesome chevron-down */ font-family: 'Font Awesome 6 Free'; font-weight: 900; position: absolute; right: 20px; top: 50%; transform: translateY(-50%); transition: transform 0.2s ease; color: #1C6758; } /* Стрелка - темно-зеленая */
1187
  details[open] > summary::after { transform: translateY(-50%) rotate(180deg); }
1188
+ details[open] > summary { border-bottom: 1px solid #d1e7dd; }
1189
  details .form-content { padding: 20px; }
1190
  .color-input-group { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
1191
  .color-input-group input { flex-grow: 1; margin: 0; }
1192
+ .remove-color-btn { background-color: #f56565; padding: 6px 10px; font-size: 0.8rem; margin-top: 0; line-height: 1; } /* Удалить цвет - красный */
1193
  .remove-color-btn:hover { background-color: #e53e3e; }
1194
+ /* Кнопка "Добавить поле цвета" - синяя для контраста */
1195
+ .add-color-btn { background-color: #63b3ed; }
1196
+ .add-color-btn:hover { background-color: #4299e1; }
1197
+ .photo-preview img { max-width: 70px; max-height: 70px; border-radius: 5px; margin: 5px 5px 0 0; border: 1px solid #d1e7dd; object-fit: cover;}
1198
  .sync-buttons { display: flex; gap: 10px; margin-bottom: 20px; flex-wrap: wrap; }
1199
+ /* Кнопка "Скачать с HF" - более мягкий цвет, например, серый */
1200
+ .download-hf-button { background-color: #7a8d85; }
1201
+ .download-hf-button:hover { background-color: #5e6e68; }
1202
  .flex-container { display: flex; flex-wrap: wrap; gap: 20px; }
1203
  .flex-item { flex: 1; min-width: 350px; /* Минимальная ширина колонки */ }
1204
  .message { padding: 10px 15px; border-radius: 6px; margin-bottom: 15px; font-size: 0.9rem;}
1205
+ .message.success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb;} /* Успех - зеленый */
1206
+ .message.error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb;} /* Ошибка - красный */
1207
+ .message.warning { background-color: #fff3cd; color: #856404; border: 1px solid #ffeeba; } /* Предупреждение - желтый */
1208
  </style>
1209
  </head>
1210
  <body>
1211
  <div class="container">
1212
  <div class="header">
1213
  <h1><i class="fas fa-tools"></i> Админ-панель Soola Cosmetics</h1>
1214
+ <a href="{{ url_for('catalog') }}" class="button" style="background-color: #3D8361;"><i class="fas fa-store"></i> Перейти в каталог</a> {# Средне-зеленая кнопка каталога #}
1215
  </div>
1216
 
1217
  {# Сообщения об успехе/ошибке #}
 
1227
  <h2><i class="fas fa-sync-alt"></i> Синхронизация с Hugging Face</h2>
1228
  <div class="sync-buttons">
1229
  <form method="POST" action="{{ url_for('force_upload') }}" style="display: inline;" onsubmit="return confirm('Вы уверены, что хотите принудительно загрузить локальные данные на сервер? Это перезапишет данные на сервере.');">
1230
+ <button type="submit" class="button" title="Загрузить локальные файлы на Hugging Face"><i class="fas fa-upload"></i> Загрузить на HF</button> {# Основная темно-зеленая кнопка #}
1231
  </form>
1232
  <form method="POST" action="{{ url_for('force_download') }}" style="display: inline;" onsubmit="return confirm('Вы уверены, что хотите принудительно скачать данные с сервера? Это перезапишет ваши локальные файлы.');">
1233
+ <button type="submit" class="button download-hf-button" title="Скачать файлы с Hugging Face (перезапишет локальные)"><i class="fas fa-download"></i> Скачать с HF</button> {# Серая кнопка #}
1234
  </form>
1235
  </div>
1236
+ <p style="font-size: 0.85rem; color: #5e6e68;">Резервное копирование на Hugging Face происходит автоматически каждые 30 минут, а также после каждого сохранения данных. Используйте эти кнопки для немедленной синхронизации.</p>
1237
  </div>
1238
 
1239
 
 
1248
  <input type="hidden" name="action" value="add_category">
1249
  <label for="add_category_name">Название новой категории:</label>
1250
  <input type="text" id="add_category_name" name="category_name" required>
1251
+ <button type="submit" class="add-button"><i class="fas fa-plus"></i> Добавить</button> {# Ярко-зеленая кнопка #}
1252
  </form>
1253
  </div>
1254
  </details>
 
1262
  <form method="POST" style="margin: 0;" onsubmit="return confirm('Вы уверены, что хотите удалить категорию \'{{ category }}\'? Товары этой категории будут помечены как \'Без категории\'.');">
1263
  <input type="hidden" name="action" value="delete_category">
1264
  <input type="hidden" name="category_name" value="{{ category }}">
1265
+ <button type="submit" class="delete-button" style="padding: 5px 10px; font-size: 0.8rem; margin: 0;"><i class="fas fa-trash-alt"></i></button> {# Красная кнопка #}
1266
  </form>
1267
  </div>
1268
  {% endfor %}
 
1294
  <input type="text" id="country" name="country">
1295
  <label for="city">Город:</label>
1296
  <input type="text" id="city" name="city">
1297
+ <button type="submit" class="add-button"><i class="fas fa-save"></i> Сохранить пользователя</button> {# Ярко-зеленая кнопка #}
1298
  </form>
1299
  </div>
1300
  </details>
 
1311
  <form method="POST" style="margin: 0;" onsubmit="return confirm('Вы уверены, что хотите удалить пользователя \'{{ login }}\'?');">
1312
  <input type="hidden" name="action" value="delete_user">
1313
  <input type="hidden" name="login" value="{{ login }}">
1314
+ <button type="submit" class="delete-button"><i class="fas fa-user-slash"></i> Удалить</button> {# Красная кнопка #}
1315
  </form>
1316
  {# Можно добавить кнопку редактирования пользователя, если нужно #}
1317
  </div>
 
1352
  <div id="add-color-inputs">
1353
  <div class="color-input-group">
1354
  <input type="text" name="colors" placeholder="Например: Розовый">
1355
+ <button type="button" class="remove-color-btn" onclick="removeColorInput(this)"><i class="fas fa-times"></i></button> {# Красная кнопка #}
1356
  </div>
1357
  </div>
1358
+ <button type="button" class="button add-color-btn" style="margin-top: 5px;" onclick="addColorInput('add-color-inputs')"><i class="fas fa-palette"></i> Добавить поле для цвета/варианта</button> {# Синяя кнопка #}
1359
  <br>
1360
+ <button type="submit" class="add-button" style="margin-top: 20px;"><i class="fas fa-save"></i> Добавить товар</button> {# Ярко-зеленая кнопка #}
1361
  </form>
1362
  </div>
1363
  </details>
 
1380
  </div>
1381
  {# Информация о товаре #}
1382
  <div style="flex-grow: 1;">
1383
+ <h3 style="margin-top: 0; margin-bottom: 5px; color: #2d332f;">{{ product['name'] }}</h3> {# Цвет текста заголовка #}
1384
  <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
1385
  <p><strong>Цена:</strong> {{ "%.2f"|format(product['price']) }} {{ currency_code }}</p>
1386
  <p class="description" title="{{ product.get('description', '') }}"><strong>Описание:</strong> {{ product.get('description', 'N/A')[:150] }}{% if product.get('description', '')|length > 150 %}...{% endif %}</p>
1387
  {% set colors = product.get('colors', []) %}
1388
  <p><strong>Цвета/Вар-ты:</strong> {{ colors|select('ne', '')|join(', ') if colors|select('ne', '')|list|length > 0 else 'Нет' }}</p>
1389
  {% if product.get('photos') and product['photos']|length > 1 %}
1390
+ <p style="font-size: 0.8rem; color: #5e6e68;">(Всего фото: {{ product['photos']|length }})</p>
1391
  {% endif %}
1392
  </div>
1393
  </div>
1394
 
1395
  <div class="item-actions">
1396
+ <button type="button" class="button" onclick="toggleEditForm('edit-form-{{ loop.index0 }}')"><i class="fas fa-edit"></i> Редактировать</button> {# Основная темно-зеленая кнопка #}
1397
  <form method="POST" style="margin:0;" onsubmit="return confirm('Вы уверены, что хотите удалить товар \'{{ product['name'] }}\'?');">
1398
  <input type="hidden" name="action" value="delete_product">
1399
  <input type="hidden" name="index" value="{{ loop.index0 }}">
1400
+ <button type="submit" class="delete-button"><i class="fas fa-trash-alt"></i> Удалить</button> {# Красная кнопка #}
1401
  </form>
1402
  </div>
1403
 
 
1438
  {% if color.strip() %} {# Отображаем только не пустые #}
1439
  <div class="color-input-group">
1440
  <input type="text" name="colors" value="{{ color }}">
1441
+ <button type="button" class="remove-color-btn" onclick="removeColorInput(this)"><i class="fas fa-times"></i></button> {# Красная кнопка #}
1442
  </div>
1443
  {% endif %}
1444
  {% endfor %}
 
1446
  {# Добавляем одно пустое поле, если цветов нет #}
1447
  <div class="color-input-group">
1448
  <input type="text" name="colors" placeholder="Например: Красный">
1449
+ <button type="button" class="remove-color-btn" onclick="removeColorInput(this)"><i class="fas fa-times"></i></button> {# Красная кнопка #}
1450
  </div>
1451
  {% endif %}
1452
  </div>
1453
+ <button type="button" class="button add-color-btn" style="margin-top: 5px;" onclick="addColorInput('edit-color-inputs-{{ loop.index0 }}')"><i class="fas fa-palette"></i> Добавить поле для цвета</button> {# Синяя кнопка #}
1454
  <br>
1455
+ <button type="submit" class="add-button" style="margin-top: 20px;"><i class="fas fa-save"></i> Сохранить изменения</button> {# Ярко-зеленая кнопка #}
1456
  </form>
1457
  </div>
1458
  </div>
 
1478
  if (container) {
1479
  const newInputGroup = document.createElement('div');
1480
  newInputGroup.className = 'color-input-group';
1481
+ newInputGroup.innerHTML = `
1482
  <input type="text" name="colors" placeholder="Новый цвет/вариант">
1483
  <button type="button" class="remove-color-btn" onclick="removeColorInput(this)"><i class="fas fa-times"></i></button>
1484
+ `;
1485
  container.appendChild(newInputGroup);
1486
  // Установить фокус на новый инпут
1487
  const newInput = newInputGroup.querySelector('input[name="colors"]');
 
1515
  """Админ-панель для управления товарами, категориями и пользователями."""
1516
  # Здесь должна быть проверка прав администратора!
1517
  # Пример: if session.get('user') != 'admin_login': return "Доступ запрещен", 403
1518
+ # Для простоты пока опускаем (В ПРОДАВКШЕНЕ ОБЯЗАТЕЛЬНО ДОБАВИТЬ!)
1519
+ if not session.get('user'): # Простейшая проверка - залогинен ли хоть кто-то
1520
+ flash("Требуется вход для доступа к админ-панели.", 'warning')
1521
+ return redirect(url_for('login'))
1522
+ # TODO: Добавить более строгую проверку роли администратора, если пользователей много
1523
 
1524
  data = load_data()
1525
  products = data.get('products', [])
 
1620
  flash(f"Ошибка при загрузке фото {photo.filename}.", 'error')
1621
  elif photo and not photo.filename:
1622
  logging.warning("Получен пустой объект файла фото при добавлении товара.")
1623
+ # Удаляем временную папку, если она пуста
1624
+ try:
1625
+ if not os.listdir(uploads_dir):
1626
+ os.rmdir(uploads_dir)
1627
+ except OSError as e:
1628
+ logging.warning(f"Не удалось удалить временную папку {uploads_dir}: {e}")
1629
+
1630
 
1631
  new_product = {
1632
  'name': name, 'price': price, 'description': description,
 
1703
  except Exception as e:
1704
  logging.error(f"Ошибка загрузки нового фото {photo.filename}: {e}", exc_info=True)
1705
  flash(f"Ошибка при загрузке нового фото {photo.filename}.", 'error')
1706
+ # Удаляем временную папку, если она пуста
1707
+ try:
1708
+ if not os.listdir(uploads_dir):
1709
+ os.rmdir(uploads_dir)
1710
+ except OSError as e:
1711
+ logging.warning(f"Не удалось удалить временную папку {uploads_dir}: {e}")
1712
 
1713
  # Если были успешно загружены новые фото, заменяем старый список
1714
  if new_photos_list:
1715
  logging.info(f"Список фото для товара {product_to_edit['name']} обновлен.")
1716
  # TODO: Удалить старые фото с HF? Это сложнее, требует хранения списка старых фото.
1717
+ # ----- Начало: Опциональное удаление старых фото ----
1718
+ old_photos = product_to_edit.get('photos', [])
1719
+ if old_photos:
1720
+ logging.info(f"Попытка удаления старых фото: {old_photos}")
1721
+ try:
1722
+ api.delete_files(
1723
+ repo_id=REPO_ID,
1724
+ paths_in_repo=[f"photos/{p}" for p in old_photos],
1725
+ repo_type="dataset",
1726
+ token=HF_TOKEN_WRITE,
1727
+ commit_message=f"Delete old photos for product {product_to_edit['name']}"
1728
+ )
1729
+ logging.info(f"Старые фото для товара {product_to_edit['name']} удалены с HF.")
1730
+ except Exception as e:
1731
+ logging.error(f"Ошибка при удалении старых фото {old_photos} с HF: {e}", exc_info=True)
1732
+ flash("Не удалось удалить старые фотографии с сервера.", "warning")
1733
+ # ----- Конец: Опциональное удаление старых фото -----
1734
  product_to_edit['photos'] = new_photos_list
1735
  flash("Фотографии товара успешно обновлены.", "success")
1736
  elif uploaded_count == 0 and any(f.filename for f in photos_files):
 
1752
  index = int(index_str)
1753
  if not (0 <= index < len(products)): raise IndexError("Индекс вне диапазона")
1754
  deleted_product = products.pop(index)
 
 
1755
  product_name = deleted_product.get('name', 'N/A')
1756
+
1757
+ # ----- Начало: Удаление фото с HF при удалении товара ----
1758
+ photos_to_delete = deleted_product.get('photos', [])
1759
+ if photos_to_delete and HF_TOKEN_WRITE:
1760
+ logging.info(f"Попытка удаления фото товара '{product_name}' с HF: {photos_to_delete}")
1761
+ try:
1762
+ api = HfApi()
1763
+ api.delete_files(
1764
+ repo_id=REPO_ID,
1765
+ paths_in_repo=[f"photos/{p}" for p in photos_to_delete],
1766
+ repo_type="dataset",
1767
+ token=HF_TOKEN_WRITE,
1768
+ commit_message=f"Delete photos for deleted product {product_name}"
1769
+ )
1770
+ logging.info(f"Фото товара '{product_name}' удалены с HF.")
1771
+ except Exception as e:
1772
+ logging.error(f"Ошибка при удалении фото {photos_to_delete} для товара '{product_name}' с HF: {e}", exc_info=True)
1773
+ flash(f"Не удалось удалить фото для товара '{product_name}' с сервера.", "warning")
1774
+ # ----- Конец: Удаление фото с HF при удалении товара ----
1775
+
1776
+ save_data(data)
1777
  logging.info(f"Товар '{product_name}' (индекс {index}) удален.")
1778
  flash(f"Товар '{product_name}' удален.", 'success')
1779
  except (ValueError, IndexError):
 
1849
  )
1850
 
1851
  # --- Маршруты для принудительной синхронизации ---
 
 
1852
 
1853
  @app.route('/force_upload', methods=['POST'])
1854
  def force_upload():
1855
+ # TODO: Добавить проверку прав администратора
1856
+ if not session.get('user'):
1857
+ flash("Требуется вход для выполнения этого действия.", 'warning')
1858
+ return redirect(url_for('login'))
1859
+
1860
  logging.info("Запущена принудительная загрузка данных на Hugging Face...")
1861
  try:
1862
  upload_db_to_hf()
 
1868
 
1869
  @app.route('/force_download', methods=['POST'])
1870
  def force_download():
1871
+ # TODO: Добавить проверку прав администратора
1872
+ if not session.get('user'):
1873
+ flash("Требуется вход для выполнения этого действия.", 'warning')
1874
+ return redirect(url_for('login'))
1875
+
1876
  logging.info("Запущено принудительное скачивание данных с Hugging Face...")
1877
  try:
1878
  download_db_from_hf()
 
1904
  # debug=False для продакшена! Установите в True только для локальной разработки.
1905
  # Использование app.run() подходит только для разработки.
1906
  # Для продакшена используйте WSGI сервер, например, Gunicorn или Waitress.
1907
+ # Пример с Gunicorn: gunicorn --bind 0.0.0.0:7860 app:app
1908
+ # Пример с Waitress: waitress-serve --host 0.0.0.0 --port 7860 app:app
1909
  app.run(debug=False, host='0.0.0.0', port=port)
1910
+