Update app.py
Browse files
app.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
|
| 2 |
-
|
|
|
|
| 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: #
|
| 282 |
-
body.dark-mode { background: #
|
| 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 #
|
| 285 |
-
body.dark-mode .header { border-bottom-color: #
|
| 286 |
-
.header h1 { font-size: 1.8rem; font-weight: 600; color: #
|
| 287 |
.auth-links { display: flex; gap: 15px; align-items: center; }
|
| 288 |
-
.auth-links a { color: #
|
| 289 |
.auth-links a:hover { text-decoration: underline; }
|
| 290 |
-
body.dark-mode .auth-links a { color: #
|
| 291 |
.auth-links span { font-weight: 500; }
|
| 292 |
-
body.dark-mode .auth-links span { color: #
|
| 293 |
-
.theme-toggle { background: none; border: none; font-size: 1.5rem; cursor: pointer; color: #
|
| 294 |
-
.theme-toggle:hover { color: #
|
| 295 |
-
body.dark-mode .theme-toggle { color: #
|
| 296 |
-
body.dark-mode .theme-toggle:hover { color: #
|
| 297 |
-
.store-address { padding: 15px; text-align: center; background-color: #
|
| 298 |
-
body.dark-mode .store-address { background-color: #
|
|
|
|
|
|
|
| 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 #
|
| 302 |
-
body.dark-mode #search-input { background-color: #
|
| 303 |
-
#search-input:focus { border-color: #
|
| 304 |
-
body.dark-mode #search-input:focus { border-color: #
|
| 305 |
-
.category-filter { padding: 8px 16px; border: 1px solid #
|
| 306 |
-
body.dark-mode .category-filter { background-color: #
|
| 307 |
-
.category-filter.active, .category-filter:hover { background-color: #
|
| 308 |
-
body.dark-mode .category-filter.active, body.dark-mode .category-filter:hover { background-color: #
|
|
|
|
|
|
|
| 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;
|
| 312 |
-
body.dark-mode .product { background: #
|
| 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;
|
| 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: #
|
| 320 |
-
body.dark-mode .product h2 { color: #
|
| 321 |
-
.product-price { font-size: 1.2rem; color: #
|
| 322 |
-
body.dark-mode .product-price { color: #
|
| 323 |
-
.product-description { font-size: 0.85rem; color: #
|
| 324 |
-
body.dark-mode .product-description { color: #
|
| 325 |
-
.product-actions { padding: 0 15px 15px 15px;
|
| 326 |
-
.product-button { display: block; width: 100%; padding: 10px; border: none; border-radius: 8px; background-color: #
|
| 327 |
-
.product-button:hover { background-color: #
|
| 328 |
.product-button i { margin-right: 5px; }
|
| 329 |
-
|
|
|
|
|
|
|
| 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: #
|
| 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: #
|
| 336 |
-
body.dark-mode .modal-content { background: #
|
| 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: #
|
| 341 |
-
body.dark-mode .close:hover { color: #
|
| 342 |
-
.modal-content h2 { margin-top: 0; margin-bottom: 20px; color: #
|
| 343 |
-
body.dark-mode .modal-content h2 { color: #
|
| 344 |
-
.cart-item { display: grid; grid-template-columns: auto 1fr auto auto; gap: 15px; align-items: center; padding: 15px 0; border-bottom: 1px solid #
|
| 345 |
-
body.dark-mode .cart-item { border-bottom-color: #
|
| 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: #
|
| 351 |
-
body.dark-mode .cart-item-price { color: #
|
| 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 #
|
| 356 |
-
body.dark-mode .quantity-input, body.dark-mode .color-select { background-color: #
|
| 357 |
-
.cart-summary { margin-top: 20px; text-align: right; border-top: 1px solid #
|
| 358 |
-
body.dark-mode .cart-summary { border-top-color: #
|
| 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: #
|
| 363 |
-
.clear-cart:hover { background-color: #
|
| 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 |
-
|
|
|
|
|
|
|
| 367 |
.notification.show { opacity: 1;}
|
| 368 |
-
.no-results-message { grid-column: 1 / -1; text-align: center; padding: 40px; font-size: 1.1rem; color: #
|
| 369 |
-
body.dark-mode .no-results-message { color: #
|
| 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 |
-
//
|
| 795 |
-
|
| 796 |
-
const whatsappUrl = `https://api.whatsapp.com/send?phone=${whatsappNumber}&text=${encodeURIComponent(orderText)}`;
|
|
|
|
| 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: #
|
| 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: #
|
| 954 |
-
<div class="swiper-button-prev" style="color: #
|
| 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: #
|
| 963 |
{% else %}
|
| 964 |
-
<p><strong>Цена:</strong> <a href="{{ url_for('login') }}" style="color: #
|
| 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, #
|
| 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: #
|
| 998 |
-
label { display: block; text-align: left; margin: 15px 0 5px; font-weight: 500; color: #
|
| 999 |
-
input[type="text"], input[type="password"] { width: 100%; padding: 12px; margin-bottom: 15px; border: 1px solid #
|
| 1000 |
-
input:focus { border-color: #
|
| 1001 |
-
button { width: 100%; padding: 12px; background-color: #
|
| 1002 |
-
button:hover { background-color: #
|
| 1003 |
-
.error { color: #
|
| 1004 |
-
.back-link { display: inline-block; margin-top: 20px; color: #
|
| 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 |
-
# Шаблон админ-панели
|
| 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: #
|
| 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 #
|
| 1142 |
-
h1, h2, h3 { font-weight: 600; color: #
|
| 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: #
|
| 1146 |
-
|
| 1147 |
form { margin-bottom: 20px; }
|
| 1148 |
-
label { font-weight: 500; margin-top: 10px; display: block; color: #
|
| 1149 |
-
input[type="text"], input[type="number"], input[type="password"], textarea, select { width: 100%; padding: 10px 12px; margin-top: 5px; border: 1px solid #
|
| 1150 |
-
input:focus, textarea:focus, select:focus { border-color: #
|
| 1151 |
textarea { min-height: 80px; resize: vertical; }
|
| 1152 |
-
|
| 1153 |
-
|
| 1154 |
-
button, .button { padding: 10px 18px; border: none; border-radius: 6px; background-color: #
|
| 1155 |
-
button:hover, .button:hover { background-color: #
|
| 1156 |
button:active, .button:active { transform: scale(0.98); }
|
| 1157 |
button[type="submit"] { min-width: 120px; justify-content: center; }
|
| 1158 |
-
.delete-button { background-color: #
|
| 1159 |
-
.delete-button:hover { background-color: #
|
| 1160 |
-
|
| 1161 |
-
|
| 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 #
|
| 1164 |
-
.item p { margin: 5px 0; font-size: 0.9rem; color: #
|
| 1165 |
-
.item strong { color: #
|
| 1166 |
-
.item .description { font-size: 0.85rem; color: #
|
| 1167 |
.item-actions { margin-top: 15px; display: flex; gap: 10px; flex-wrap: wrap; }
|
| 1168 |
-
|
| 1169 |
-
|
| 1170 |
-
|
| 1171 |
-
|
|
|
|
|
|
|
|
|
|
| 1172 |
details[open] > summary::after { transform: translateY(-50%) rotate(180deg); }
|
| 1173 |
-
details[open] > summary { border-bottom: 1px solid #
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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: #
|
| 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
|
| 1212 |
</form>
|
| 1213 |
</div>
|
| 1214 |
-
<p style="font-size: 0.85rem; color: #
|
| 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-
|
| 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: #
|
| 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: #
|
| 1369 |
{% endif %}
|
| 1370 |
</div>
|
| 1371 |
</div>
|
| 1372 |
|
| 1373 |
<div class="item-actions">
|
| 1374 |
-
|
| 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-
|
| 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
|
| 1827 |
-
# Пример с Waitress: waitress-serve --host 0.0.0.0 --port 7860
|
| 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 |
+
|