Kgshop commited on
Commit
a1c1baf
·
verified ·
1 Parent(s): 5b5714b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +289 -167
app.py CHANGED
@@ -12,28 +12,39 @@ from werkzeug.utils import secure_filename
12
  from dotenv import load_dotenv
13
  import requests
14
  import uuid
 
15
  load_dotenv()
 
16
  app = Flask(__name__)
17
- app.secret_key = 'your_unique_secret_key_soola_cosmetics_67890_no_login'
18
  DATA_FILE = 'data.json'
 
19
  SYNC_FILES = [DATA_FILE]
 
20
  REPO_ID = "Kgshop/nizhbel"
21
  HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
22
  HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
 
23
  STORE_ADDRESS = "Рынок Кербент, 6 ряд , 3 контейнер / 5 ряд 25 контейнер "
 
24
  CURRENCY_CODE = 'KGS'
25
  CURRENCY_NAME = 'Кыргызский сом'
 
26
  DOWNLOAD_RETRIES = 3
27
  DOWNLOAD_DELAY = 5
28
- WHATSAPP_NUMBER = "996509455959"
29
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
 
30
  def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY):
31
  if not HF_TOKEN_READ and not HF_TOKEN_WRITE:
32
  logging.warning("HF_TOKEN_READ/HF_TOKEN_WRITE not set. Download might fail for private repos.")
 
33
  token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE
 
34
  files_to_download = [specific_file] if specific_file else SYNC_FILES
35
  logging.info(f"Attempting download for {files_to_download} from {REPO_ID}...")
36
  all_successful = True
 
37
  for file_name in files_to_download:
38
  success = False
39
  for attempt in range(retries + 1):
@@ -74,13 +85,17 @@ def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWN
74
  logging.error(f"Network error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...")
75
  except Exception as e:
76
  logging.error(f"Unexpected error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...", exc_info=True)
 
77
  if attempt < retries:
78
  time.sleep(delay)
 
79
  if not success:
80
  logging.error(f"Failed to download {file_name} after {retries + 1} attempts.")
81
  all_successful = False
 
82
  logging.info(f"Download process finished. Overall success: {all_successful}")
83
  return all_successful
 
84
  def upload_db_to_hf(specific_file=None):
85
  if not HF_TOKEN_WRITE:
86
  logging.warning("HF_TOKEN (for writing) not set. Skipping upload to Hugging Face.")
@@ -89,6 +104,7 @@ def upload_db_to_hf(specific_file=None):
89
  api = HfApi()
90
  files_to_upload = [specific_file] if specific_file else SYNC_FILES
91
  logging.info(f"Starting upload of {files_to_upload} to HF repo {REPO_ID}...")
 
92
  for file_name in files_to_upload:
93
  if os.path.exists(file_name):
94
  try:
@@ -108,6 +124,7 @@ def upload_db_to_hf(specific_file=None):
108
  logging.info("Finished uploading files to HF.")
109
  except Exception as e:
110
  logging.error(f"General error during Hugging Face upload initialization or process: {e}", exc_info=True)
 
111
  def periodic_backup():
112
  backup_interval = 1800
113
  logging.info(f"Setting up periodic backup every {backup_interval} seconds.")
@@ -116,6 +133,7 @@ def periodic_backup():
116
  logging.info("Starting periodic backup...")
117
  upload_db_to_hf()
118
  logging.info("Periodic backup finished.")
 
119
  def load_data():
120
  default_data = {'products': [], 'categories': [], 'orders': {}}
121
  try:
@@ -133,6 +151,7 @@ def load_data():
133
  logging.warning(f"Local file {DATA_FILE} not found. Attempting download from HF.")
134
  except json.JSONDecodeError:
135
  logging.error(f"Error decoding JSON in local {DATA_FILE}. File might be corrupt. Attempting download.")
 
136
  if download_db_from_hf(specific_file=DATA_FILE):
137
  try:
138
  with open(DATA_FILE, 'r', encoding='utf-8') as file:
@@ -164,6 +183,8 @@ def load_data():
164
  except Exception as create_e:
165
  logging.error(f"Failed to create empty local file {DATA_FILE}: {create_e}")
166
  return default_data
 
 
167
  def save_data(data):
168
  try:
169
  if not isinstance(data, dict):
@@ -172,12 +193,15 @@ def save_data(data):
172
  if 'products' not in data: data['products'] = []
173
  if 'categories' not in data: data['categories'] = []
174
  if 'orders' not in data: data['orders'] = {}
 
175
  with open(DATA_FILE, 'w', encoding='utf-8') as file:
176
  json.dump(data, file, ensure_ascii=False, indent=4)
177
  logging.info(f"Data successfully saved to {DATA_FILE}")
178
  upload_db_to_hf(specific_file=DATA_FILE)
179
  except Exception as e:
180
  logging.error(f"Error saving data to {DATA_FILE}: {e}", exc_info=True)
 
 
181
  CATALOG_TEMPLATE = '''
182
  <!DOCTYPE html>
183
  <html lang="ru">
@@ -190,89 +214,67 @@ CATALOG_TEMPLATE = '''
190
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.css">
191
  <style>
192
  * { margin: 0; padding: 0; box-sizing: border-box; }
193
- body { font-family: 'Poppins', sans-serif; background: #d7f7e0; color: #1a362d; line-height: 1.6; transition: background 0.3s, color 0.3s; }
194
- body.dark-mode { background: #1a362d; color: #d7f7e0; }
195
  .container { max-width: 1300px; margin: 0 auto; padding: 20px; }
196
- .header { display: flex; justify-content: space-between; align-items: center; padding: 15px 0; border-bottom: 1px solid #9cdbb0; }
197
- body.dark-mode .header { border-bottom-color: #4f6a5a; }
198
- .header h1 { font-size: 1.8rem; font-weight: 600; color: #4CAF50; }
199
- .theme-toggle { background: none; border: none; font-size: 1.5rem; cursor: pointer; color: #7ed99c; transition: color 0.3s ease; }
200
- .theme-toggle:hover { color: #388E3C; }
201
- body.dark-mode .theme-toggle { color: #a7f3d0; }
202
- body.dark-mode .theme-toggle:hover { color: #d7f7e0; }
203
- .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: #4f6a5a; }
204
- body.dark-mode .store-address { background-color: #0f231c; color: #d7f7e0; }
205
  .filters-container { margin: 20px 0; display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }
206
  .search-container { margin: 20px 0; text-align: center; }
207
- #search-input { width: 90%; max-width: 600px; padding: 12px 18px; font-size: 1rem; border: 1px solid #9cdbb0; border-radius: 25px; outline: none; box-shadow: 0 2px 5px rgba(0,0,0,0.05); transition: all 0.3s ease; }
208
- body.dark-mode #search-input { background-color: #0f231c; border-color: #4f6a5a; color: #d7f7e0; }
209
- #search-input:focus { border-color: #4CAF50; box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.2); }
210
- body.dark-mode #search-input:focus { border-color: #388E3C; box-shadow: 0 0 0 3px rgba(56, 142, 60, 0.3); }
211
- .category-filter { padding: 8px 16px; border: 1px solid #9cdbb0; 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: #388E3C; }
212
- body.dark-mode .category-filter { background-color: #0f231c; border-color: #4f6a5a; color: #a7f3d0; }
213
- .category-filter.active, .category-filter:hover { background-color: #4CAF50; color: white; border-color: #4CAF50; box-shadow: 0 2px 10px rgba(76, 175, 80, 0.3); }
214
- body.dark-mode .category-filter.active, body.dark-mode .category-filter:hover { background-color: #388E3C; border-color: #388E3C; color: #1a362d; box-shadow: 0 2px 10px rgba(56, 142, 60, 0.4); }
215
  .products-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 20px; padding: 10px; }
216
  @media (min-width: 600px) { .products-grid { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); } }
217
  @media (min-width: 900px) { .products-grid { grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); } }
218
- .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 #c1e7cc;}
219
- body.dark-mode .product { background: #0f231c; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); border-color: #4f6a5a; }
220
- .product:hover { transform: translateY(-5px) scale(1.02); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12); }
221
- body.dark-mode .product:hover { box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3); }
222
  .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; }
223
  .product-image img { max-width: 100%; max-height: 100%; object-fit: contain; transition: transform 0.3s ease; }
224
  .product-info { padding: 15px; flex-grow: 1; display: flex; flex-direction: column; justify-content: center; }
225
- .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: #1a362d; }
226
- body.dark-mode .product h2 { color: #d7f7e0; }
227
- .product-price { font-size: 1.2rem; color: #388E3C; font-weight: 700; text-align: center; margin: 5px 0; }
228
- body.dark-mode .product-price { color: #a7f3d0; }
229
- .product-description { font-size: 0.85rem; color: #4f6a5a; text-align: center; margin-bottom: 15px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
230
- body.dark-mode .product-description { color: #c1e7cc; }
231
  .product-actions { padding: 0 15px 15px 15px; display: flex; flex-direction: column; gap: 8px; }
232
- .product-button { display: block; width: 100%; padding: 10px; border: none; border-radius: 8px; background-color: #4CAF50; 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; }
233
- .product-button:hover { background-color: #388E3C; box-shadow: 0 4px 15px rgba(56, 142, 60, 0.4); transform: translateY(-2px); }
234
  .product-button i { margin-right: 5px; }
235
- .add-to-cart { background-color: #4CAF50; }
236
- .add-to-cart:hover { background-color: #388E3C; box-shadow: 0 4px 15px rgba(56, 142, 60, 0.4); }
237
- #cart-button { position: fixed; bottom: 25px; right: 25px; background-color: #4CAF50; 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(76, 175, 80, 0.4); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); z-index: 1000; }
 
238
  #cart-button .fa-shopping-cart { margin-right: 0; }
239
- #cart-button span { position: absolute; top: -5px; right: -5px; background-color: #388E3C; color: white; border-radius: 50%; padding: 2px 6px; font-size: 0.7rem; font-weight: bold; }
240
- .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; }
241
- .modal-content { background: #ffffff; 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; }
242
- body.dark-mode .modal-content { background: #0f231c; color: #d7f7e0; }
243
  @keyframes slideIn { from { transform: translateY(-30px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
244
  .close { position: absolute; top: 15px; right: 15px; font-size: 1.8rem; color: #aaa; cursor: pointer; transition: color 0.3s; line-height: 1; }
245
- .close:hover { color: #333; }
246
- body.dark-mode .close { color: #c1e7cc; }
247
- body.dark-mode .close:hover { color: #ffffff; }
248
- .modal-content h2 { margin-top: 0; margin-bottom: 20px; color: #4CAF50; display: flex; align-items: center; gap: 10px;}
249
- body.dark-mode .modal-content h2 { color: #a7f3d0; }
250
- .cart-item { display: grid; grid-template-columns: auto 1fr auto auto; gap: 15px; align-items: center; padding: 15px 0; border-bottom: 1px solid #9cdbb0; }
251
- body.dark-mode .cart-item { border-bottom-color: #4f6a5a; }
252
  .cart-item:last-child { border-bottom: none; }
253
- .cart-item img { width: 60px; height: 60px; object-fit: contain; border-radius: 8px; background-color: #fff; padding: 5px; grid-column: 1; }
254
  .cart-item-details { grid-column: 2; }
255
- .cart-item-details strong { display: block; margin-bottom: 5px; font-size: 1rem; }
256
- .cart-item-price { font-size: 0.9rem; color: #4f6a5a; }
257
- body.dark-mode .cart-item-price { color: #c1e7cc; }
258
- .cart-item-total { font-weight: bold; text-align: right; grid-column: 3; font-size: 1rem;}
259
- .cart-item-remove { grid-column: 4; background:none; border:none; color:#f56565; cursor:pointer; font-size: 1.3em; padding: 5px; line-height: 1; }
260
- .cart-item-remove:hover { color: #c53030; }
261
- .quantity-input, .color-select { width: 100%; max-width: 180px; padding: 10px; border: 1px solid #9cdbb0; border-radius: 8px; font-size: 1rem; margin: 10px 0; box-sizing: border-box; }
262
- body.dark-mode .quantity-input, body.dark-mode .color-select { background-color: #1a362d; border-color: #4f6a5a; color: #d7f7e0; }
263
- .cart-summary { margin-top: 20px; text-align: right; border-top: 1px solid #9cdbb0; padding-top: 15px; }
264
- body.dark-mode .cart-summary { border-top-color: #4f6a5a; }
265
- .cart-summary strong { font-size: 1.2rem; }
266
  .cart-actions { margin-top: 25px; display: flex; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
267
  .cart-actions .product-button { width: auto; flex-grow: 1; }
268
- .clear-cart { background-color: #a0aec0; }
269
- .clear-cart:hover { background-color: #718096; box-shadow: 0 4px 15px rgba(113, 128, 150, 0.4); }
270
- .formulate-order-button { background-color: #4CAF50; }
271
- .formulate-order-button:hover { background-color: #388E3C; box-shadow: 0 4px 15px rgba(56, 142, 60, 0.4); }
272
- .notification { position: fixed; bottom: 80px; left: 50%; transform: translateX(-50%); background-color: #4CAF50; 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;}
273
  .notification.show { opacity: 1;}
274
- .no-results-message { grid-column: 1 / -1; text-align: center; padding: 40px; font-size: 1.1rem; color: #a0aec0; }
275
- body.dark-mode .no-results-message { color: #c1e7cc; }
276
  .top-product-indicator { position: absolute; top: 8px; right: 8px; background-color: rgba(255, 215, 0, 0.8); color: #333; padding: 2px 6px; font-size: 0.7rem; border-radius: 4px; font-weight: bold; z-index: 10; backdrop-filter: blur(2px); }
277
  .product { position: relative; }
278
  </style>
@@ -281,23 +283,23 @@ CATALOG_TEMPLATE = '''
281
  <div class="container">
282
  <div class="header">
283
  <div class="logo-title-container" style="display: flex; align-items: center; gap: 15px;">
284
- <img src="https://upload.wikimedia.org/wikipedia/commons/thumb/a/ab/Apple-logo.png/2560px-Apple-logo.png" alt="Meka Shop Logo" style="height: 40px; width: auto; border-radius: 4px;">
285
  <h1>Meka Shop</h1>
286
  </div>
287
- <button class="theme-toggle" onclick="toggleTheme()" aria-label="Переключить тему">
288
- <i class="fas fa-moon"></i>
289
- </button>
290
  </div>
 
291
  <div class="store-address">Наш адрес: {{ store_address }}</div>
 
292
  <div class="filters-container">
293
  <button class="category-filter active" data-category="all">Все категории</button>
294
  {% for category in categories %}
295
  <button class="category-filter" data-category="{{ category }}">{{ category }}</button>
296
  {% endfor %}
297
  </div>
 
298
  <div class="search-container">
299
  <input type="text" id="search-input" placeholder="Поиск по названию или описанию...">
300
  </div>
 
301
  <div class="products-grid" id="products-grid">
302
  {% for product in products %}
303
  <div class="product"
@@ -334,12 +336,14 @@ CATALOG_TEMPLATE = '''
334
  {% endif %}
335
  </div>
336
  </div>
 
337
  <div id="productModal" class="modal">
338
  <div class="modal-content">
339
  <span class="close" onclick="closeModal('productModal')" aria-label="Закрыть">×</span>
340
  <div id="modalContent">Загрузка...</div>
341
  </div>
342
  </div>
 
343
  <div id="quantityModal" class="modal">
344
  <div class="modal-content">
345
  <span class="close" onclick="closeModal('quantityModal')" aria-label="Закрыть">×</span>
@@ -351,6 +355,7 @@ CATALOG_TEMPLATE = '''
351
  <button class="product-button add-to-cart" onclick="confirmAddToCart()"><i class="fas fa-check"></i> Добавить в корзину</button>
352
  </div>
353
  </div>
 
354
  <div id="cartModal" class="modal">
355
  <div class="modal-content">
356
  <span class="close" onclick="closeModal('cartModal')" aria-label="Закрыть">×</span>
@@ -369,34 +374,23 @@ CATALOG_TEMPLATE = '''
369
  </div>
370
  </div>
371
  </div>
 
372
  <button id="cart-button" onclick="openCartModal()" aria-label="Открыть корзину">
373
  <i class="fas fa-shopping-cart"></i>
374
  <span id="cart-count">0</span>
375
  </button>
 
376
  <div id="notification-placeholder"></div>
 
377
  <script src="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.js"></script>
378
  <script>
379
  const products = {{ products|tojson }};
380
  const repoId = '{{ repo_id }}';
381
  const currencyCode = '{{ currency_code }}';
382
  let selectedProductIndex = null;
383
- let cart = JSON.parse(localStorage.getItem('soolaCart') || '[]');
384
- function toggleTheme() {
385
- document.body.classList.toggle('dark-mode');
386
- const icon = document.querySelector('.theme-toggle i');
387
- const isDarkMode = document.body.classList.contains('dark-mode');
388
- icon.classList.toggle('fa-moon', !isDarkMode);
389
- icon.classList.toggle('fa-sun', isDarkMode);
390
- localStorage.setItem('soolaTheme', isDarkMode ? 'dark' : 'light');
391
- }
392
- function applyInitialTheme() {
393
- const savedTheme = localStorage.getItem('soolaTheme');
394
- if (savedTheme === 'dark') {
395
- document.body.classList.add('dark-mode');
396
- const icon = document.querySelector('.theme-toggle i');
397
- if (icon) icon.classList.replace('fa-moon', 'fa-sun');
398
- }
399
- }
400
  function openModal(index) {
401
  loadProductDetails(index);
402
  const modal = document.getElementById('productModal');
@@ -405,6 +399,7 @@ CATALOG_TEMPLATE = '''
405
  document.body.style.overflow = 'hidden';
406
  }
407
  }
 
408
  function closeModal(modalId) {
409
  const modal = document.getElementById(modalId);
410
  if (modal) {
@@ -415,6 +410,7 @@ CATALOG_TEMPLATE = '''
415
  document.body.style.overflow = 'auto';
416
  }
417
  }
 
418
  function loadProductDetails(index) {
419
  const modalContent = document.getElementById('modalContent');
420
  if (!modalContent) return;
@@ -430,9 +426,10 @@ CATALOG_TEMPLATE = '''
430
  })
431
  .catch(error => {
432
  console.error('Ошибка загрузки деталей продукта:', error);
433
- modalContent.innerHTML = `<p style="color: red; text-align:center; padding: 40px;">Не удалось загрузить информацию о товаре. ${error.message}</p>`;
434
  });
435
  }
 
436
  function initializeSwiper() {
437
  const swiperContainer = document.querySelector('#productModal .swiper-container');
438
  if (swiperContainer) {
@@ -448,6 +445,7 @@ CATALOG_TEMPLATE = '''
448
  });
449
  }
450
  }
 
451
  function openQuantityModal(index) {
452
  selectedProductIndex = index;
453
  const product = products[index];
@@ -456,10 +454,13 @@ CATALOG_TEMPLATE = '''
456
  alert("Ошибка: товар не найден.");
457
  return;
458
  }
 
459
  const colorSelect = document.getElementById('colorSelect');
460
  const colorLabel = document.querySelector('label[for="colorSelect"]');
461
  colorSelect.innerHTML = '';
 
462
  const validColors = product.colors ? product.colors.filter(c => c && c.trim() !== "") : [];
 
463
  if (validColors.length > 0) {
464
  validColors.forEach(color => {
465
  const option = document.createElement('option');
@@ -473,6 +474,7 @@ CATALOG_TEMPLATE = '''
473
  colorSelect.style.display = 'none';
474
  if(colorLabel) colorLabel.style.display = 'none';
475
  }
 
476
  document.getElementById('quantityInput').value = 1;
477
  const modal = document.getElementById('quantityModal');
478
  if(modal) {
@@ -480,24 +482,30 @@ CATALOG_TEMPLATE = '''
480
  document.body.style.overflow = 'hidden';
481
  }
482
  }
 
483
  function confirmAddToCart() {
484
  if (selectedProductIndex === null) return;
 
485
  const quantityInput = document.getElementById('quantityInput');
486
  const quantity = parseInt(quantityInput.value);
487
  const colorSelect = document.getElementById('colorSelect');
488
  const color = colorSelect.style.display !== 'none' && colorSelect.value ? colorSelect.value : 'N/A';
 
489
  if (isNaN(quantity) || quantity <= 0) {
490
  alert("Пожалуйста, укажите корректное количество (больше 0).");
491
  quantityInput.focus();
492
  return;
493
  }
 
494
  const product = products[selectedProductIndex];
495
  if (!product) {
496
  alert("Ошибка добавления: товар не найден.");
497
  return;
498
  }
 
499
  const cartItemId = `${product.name}-${color}`;
500
  const existingItemIndex = cart.findIndex(item => item.id === cartItemId);
 
501
  if (existingItemIndex > -1) {
502
  cart[existingItemIndex].quantity += quantity;
503
  } else {
@@ -510,17 +518,21 @@ CATALOG_TEMPLATE = '''
510
  color: color
511
  });
512
  }
513
- localStorage.setItem('soolaCart', JSON.stringify(cart));
 
514
  closeModal('quantityModal');
515
  updateCartButton();
516
  showNotification(`${product.name} добавлен в корзину!`);
517
  }
 
518
  function updateCartButton() {
519
  const cartCountElement = document.getElementById('cart-count');
520
  const cartButton = document.getElementById('cart-button');
521
  if (!cartCountElement || !cartButton) return;
 
522
  let totalItems = 0;
523
  cart.forEach(item => { totalItems += item.quantity; });
 
524
  if (totalItems > 0) {
525
  cartCountElement.textContent = totalItems;
526
  cartButton.style.display = 'flex';
@@ -529,11 +541,14 @@ CATALOG_TEMPLATE = '''
529
  cartButton.style.display = 'none';
530
  }
531
  }
 
532
  function openCartModal() {
533
  const cartContent = document.getElementById('cartContent');
534
  const cartTotalElement = document.getElementById('cartTotal');
535
  if (!cartContent || !cartTotalElement) return;
 
536
  let total = 0;
 
537
  if (cart.length === 0) {
538
  cartContent.innerHTML = '<p style="text-align: center; padding: 20px;">Ваша корзина пуста.</p>';
539
  cartTotalElement.textContent = '0.00';
@@ -545,6 +560,7 @@ CATALOG_TEMPLATE = '''
545
  ? `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${item.photo}`
546
  : 'https://via.placeholder.com/60x60.png?text=N/A';
547
  const colorText = item.color !== 'N/A' ? ` (Цвет: ${item.color})` : '';
 
548
  return `
549
  <div class="cart-item">
550
  <img src="${photoUrl}" alt="${item.name}">
@@ -565,31 +581,38 @@ CATALOG_TEMPLATE = '''
565
  document.body.style.overflow = 'hidden';
566
  }
567
  }
 
568
  function removeFromCart(itemId) {
569
  cart = cart.filter(item => item.id !== itemId);
570
- localStorage.setItem('soolaCart', JSON.stringify(cart));
571
  openCartModal();
572
  updateCartButton();
573
  }
 
574
  function clearCart() {
575
  if (confirm("Вы уверены, что хотите очистить корзину?")) {
576
  cart = [];
577
- localStorage.removeItem('soolaCart');
578
  openCartModal();
579
  updateCartButton();
580
  }
581
  }
 
582
  function formulateOrder() {
583
  if (cart.length === 0) {
584
  alert("Корзина пуста! Добавьте товары перед формированием заказа.");
585
  return;
586
  }
 
587
  const orderData = {
588
  cart: cart
589
  };
 
590
  const formulateButton = document.querySelector('.formulate-order-button');
591
  if (formulateButton) formulateButton.disabled = true;
 
592
  showNotification("Формируем заказ...", 5000);
 
593
  fetch('/create_order', {
594
  method: 'POST',
595
  headers: { 'Content-Type': 'application/json' },
@@ -603,7 +626,7 @@ CATALOG_TEMPLATE = '''
603
  })
604
  .then(data => {
605
  if (data.order_id) {
606
- localStorage.removeItem('soolaCart');
607
  cart = [];
608
  updateCartButton();
609
  closeModal('cartModal');
@@ -618,20 +641,26 @@ CATALOG_TEMPLATE = '''
618
  if (formulateButton) formulateButton.disabled = false;
619
  });
620
  }
 
 
621
  function filterProducts() {
622
  const searchTerm = document.getElementById('search-input').value.toLowerCase().trim();
623
  const activeCategoryButton = document.querySelector('.category-filter.active');
624
  const activeCategory = activeCategoryButton ? activeCategoryButton.dataset.category : 'all';
625
  const grid = document.getElementById('products-grid');
626
  let visibleProducts = 0;
 
627
  const existingNoResults = grid.querySelector('.no-results-message');
628
  if (existingNoResults) existingNoResults.remove();
 
629
  document.querySelectorAll('.products-grid .product').forEach(productElement => {
630
  const name = productElement.getAttribute('data-name');
631
  const description = productElement.getAttribute('data-description');
632
  const category = productElement.getAttribute('data-category');
 
633
  const matchesSearch = !searchTerm || name.includes(searchTerm) || description.includes(searchTerm);
634
  const matchesCategory = activeCategory === 'all' || category === activeCategory;
 
635
  if (matchesSearch && matchesCategory) {
636
  productElement.style.display = 'flex';
637
  visibleProducts++;
@@ -639,6 +668,7 @@ CATALOG_TEMPLATE = '''
639
  productElement.style.display = 'none';
640
  }
641
  });
 
642
  if (visibleProducts === 0 && products.length > 0) {
643
  const p = document.createElement('p');
644
  p.className = 'no-results-message';
@@ -651,10 +681,13 @@ CATALOG_TEMPLATE = '''
651
  grid.appendChild(p);
652
  }
653
  }
 
654
  function setupFilters() {
655
  const searchInput = document.getElementById('search-input');
656
  const categoryFilters = document.querySelectorAll('.category-filter');
 
657
  if(searchInput) searchInput.addEventListener('input', filterProducts);
 
658
  categoryFilters.forEach(filter => {
659
  filter.addEventListener('click', function() {
660
  categoryFilters.forEach(f => f.classList.remove('active'));
@@ -664,6 +697,7 @@ CATALOG_TEMPLATE = '''
664
  });
665
  filterProducts();
666
  }
 
667
  function showNotification(message, duration = 3000) {
668
  const placeholder = document.getElementById('notification-placeholder');
669
  if (!placeholder) {
@@ -677,26 +711,33 @@ CATALOG_TEMPLATE = '''
677
  document.body.appendChild(newPlaceholder);
678
  placeholder = newPlaceholder;
679
  }
 
 
680
  const notification = document.createElement('div');
681
  notification.className = 'notification';
682
  notification.textContent = message;
683
  placeholder.appendChild(notification);
 
684
  void notification.offsetWidth;
 
685
  notification.classList.add('show');
 
686
  setTimeout(() => {
687
  notification.classList.remove('show');
688
  notification.addEventListener('transitionend', () => notification.remove());
689
  }, duration);
690
  }
 
691
  document.addEventListener('DOMContentLoaded', () => {
692
- applyInitialTheme();
693
  updateCartButton();
694
  setupFilters();
 
695
  window.addEventListener('click', function(event) {
696
  if (event.target.classList.contains('modal')) {
697
  closeModal(event.target.id);
698
  }
699
  });
 
700
  window.addEventListener('keydown', function(event) {
701
  if (event.key === 'Escape') {
702
  document.querySelectorAll('.modal[style*="display: block"]').forEach(modal => {
@@ -705,14 +746,16 @@ CATALOG_TEMPLATE = '''
705
  }
706
  });
707
  });
 
708
  </script>
709
  </body>
710
  </html>
711
  '''
 
712
  PRODUCT_DETAIL_TEMPLATE = '''
713
  <div style="padding: 10px;">
714
- <h2 style="font-size: 1.6rem; font-weight: 600; margin-bottom: 15px; text-align: center; color: #4CAF50;">{{ product['name'] }}</h2>
715
- <div class="swiper-container" style="max-width: 450px; margin: 0 auto 20px; border-radius: 10px; overflow: hidden; background-color: #fff;">
716
  <div class="swiper-wrapper">
717
  {% if product.get('photos') and product['photos']|length > 0 %}
718
  {% for photo in product['photos'] %}
@@ -732,13 +775,14 @@ PRODUCT_DETAIL_TEMPLATE = '''
732
  </div>
733
  {% if product.get('photos') and product['photos']|length > 1 %}
734
  <div class="swiper-pagination" style="position: relative; bottom: 5px;"></div>
735
- <div class="swiper-button-next" style="color: #4CAF50;"></div>
736
- <div class="swiper-button-prev" style="color: #4CAF50;"></div>
737
  {% endif %}
738
  </div>
739
- <div style="margin-top: 20px; font-size: 1rem; line-height: 1.7;">
 
740
  <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
741
- <p style="font-size: 1.2rem; font-weight: bold; color: #388E3C;"><strong>Цена:</strong> {{ "%.2f"|format(product['price']) }} {{ currency_code }}</p>
742
  <p><strong>Описание:</strong><br> {{ product.get('description', 'Описание отсутствует.')|replace('\\n', '<br>')|safe }}</p>
743
  {% set colors = product.get('colors', []) %}
744
  {% if colors and colors|select('ne', '')|list|length > 0 %}
@@ -747,6 +791,7 @@ PRODUCT_DETAIL_TEMPLATE = '''
747
  </div>
748
  </div>
749
  '''
 
750
  ORDER_TEMPLATE = '''
751
  <!DOCTYPE html>
752
  <html lang="ru">
@@ -757,31 +802,31 @@ ORDER_TEMPLATE = '''
757
  <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
758
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
759
  <style>
760
- body { font-family: 'Poppins', sans-serif; background: #d7f7e0; color: #1a362d; line-height: 1.6; padding: 20px; }
761
- .container { max-width: 800px; margin: 20px auto; padding: 30px; background: #fff; border-radius: 15px; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); border: 1px solid #c1e7cc; }
762
- h1 { text-align: center; color: #4CAF50; margin-bottom: 25px; font-size: 1.8rem; font-weight: 600; }
763
- h2 { color: #388E3C; margin-top: 30px; margin-bottom: 15px; font-size: 1.4rem; border-bottom: 1px solid #9cdbb0; padding-bottom: 8px;}
764
- .order-meta { font-size: 0.9rem; color: #a0aec0; margin-bottom: 20px; text-align: center; }
765
- .order-item { display: grid; grid-template-columns: 60px 1fr auto; gap: 15px; align-items: center; padding: 15px 0; border-bottom: 1px solid #c1e7cc; }
766
  .order-item:last-child { border-bottom: none; }
767
- .order-item img { width: 60px; height: 60px; object-fit: contain; border-radius: 8px; background-color: #fff; padding: 5px; border: 1px solid #c1e7cc;}
768
- .item-details strong { display: block; margin-bottom: 5px; font-size: 1.05rem; color: #1a362d;}
769
- .item-details span { font-size: 0.9rem; color: #4f6a5a; display: block;}
770
- .item-total { font-weight: bold; text-align: right; font-size: 1rem; color: #388E3C;}
771
- .order-summary { margin-top: 30px; padding-top: 20px; border-top: 2px solid #4CAF50; text-align: right; }
772
  .order-summary p { margin-bottom: 10px; font-size: 1.1rem; }
773
- .order-summary strong { font-size: 1.3rem; color: #4CAF50; }
774
- .customer-info { margin-top: 30px; background-color: #fff; padding: 20px; border-radius: 8px; border: 1px solid #c1e7cc;}
775
  .customer-info p { margin-bottom: 8px; font-size: 0.95rem; }
776
- .customer-info strong { color: #388E3C; }
777
  .actions { margin-top: 30px; text-align: center; }
778
- .button { padding: 12px 25px; border: none; border-radius: 8px; background-color: #4CAF50; color: white; font-weight: 600; cursor: pointer; transition: background-color 0.3s ease, transform 0.1s ease; font-size: 1rem; display: inline-flex; align-items: center; gap: 8px; text-decoration: none; }
779
- .button:hover { background-color: #388E3C; }
780
  .button:active { transform: scale(0.98); }
781
  .button i { font-size: 1.2rem; }
782
- .catalog-link { display: block; text-align: center; margin-top: 25px; color: #388E3C; text-decoration: none; font-size: 0.9rem; }
783
  .catalog-link:hover { text-decoration: underline; }
784
- .not-found { text-align: center; color: #c53030; font-size: 1.2rem; padding: 40px 0;}
785
  </style>
786
  </head>
787
  <body>
@@ -789,6 +834,7 @@ ORDER_TEMPLATE = '''
789
  {% if order %}
790
  <h1><i class="fas fa-receipt"></i> Ваш Заказ №{{ order.id }}</h1>
791
  <p class="order-meta">Дата создания: {{ order.created_at }}</p>
 
792
  <h2><i class="fas fa-shopping-bag"></i> Товары в заказе</h2>
793
  <div id="orderItems">
794
  {% for item in order.cart %}
@@ -804,34 +850,42 @@ ORDER_TEMPLATE = '''
804
  </div>
805
  {% endfor %}
806
  </div>
 
807
  <div class="order-summary">
808
  <p>Общая сумма товаров: <strong>{{ "%.2f"|format(order.total_price) }} {{ currency_code }}</strong></p>
809
  <p><strong>ИТОГО К ОПЛАТЕ: {{ "%.2f"|format(order.total_price) }} {{ currency_code }}</strong></p>
810
  </div>
 
811
  <div class="customer-info">
812
  <h2><i class="fas fa-info-circle"></i> Статус заказа</h2>
813
  <p>Этот заказ был оформлен без входа в систему.</p>
814
  <p>Пожалуйста, свяжитесь с нами по WhatsApp для подтверждения и уточнения деталей.</p>
815
  </div>
 
816
  <div class="actions">
817
  <button class="button" onclick="sendOrderViaWhatsApp()"><i class="fab fa-whatsapp"></i> Отправить заказ</button>
818
  </div>
 
819
  <a href="{{ url_for('catalog') }}" class="catalog-link">← Вернуться в каталог</a>
 
820
  <script>
821
  function sendOrderViaWhatsApp() {
822
  const orderId = '{{ order.id }}';
823
  const orderUrl = `{{ request.url }}`;
824
- const whatsappNumber = "{{ whatsapp_number }}";
 
825
  let message = `Здравствуйте! Хочу подтвердить свой заказ на Meka Shop:%0A%0A`;
826
  message += `*Номер заказа:* ${orderId}%0A`;
827
  message += `*Ссылка на заказ:* ${encodeURIComponent(orderUrl)}%0A%0A`;
828
  message += `Пожалуйста, свяжитесь со мной для уточнения деталей оплаты и доставки.`;
 
829
  const whatsappUrl = `https://api.whatsapp.com/send?phone=${whatsappNumber}&text=${message}`;
830
  window.open(whatsappUrl, '_blank');
831
  }
832
  </script>
 
833
  {% else %}
834
- <h1 style="color: #c53030;"><i class="fas fa-exclamation-triangle"></i> Ошибка</h1>
835
  <p class="not-found">Заказ с таким ID не найден.</p>
836
  <a href="{{ url_for('catalog') }}" class="catalog-link">← Вернуться в каталог</a>
837
  {% endif %}
@@ -839,6 +893,7 @@ ORDER_TEMPLATE = '''
839
  </body>
840
  </html>
841
  '''
 
842
  ADMIN_TEMPLATE = '''
843
  <!DOCTYPE html>
844
  <html lang="ru">
@@ -849,77 +904,77 @@ ADMIN_TEMPLATE = '''
849
  <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
850
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
851
  <style>
852
- body { font-family: 'Poppins', sans-serif; background-color: #c1e7cc; color: #1a362d; padding: 20px; line-height: 1.6; }
853
  .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); }
854
- .header { padding-bottom: 15px; margin-bottom: 25px; border-bottom: 1px solid #9cdbb0; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px;}
855
- h1, h2, h3 { font-weight: 600; color: #4CAF50; margin-bottom: 15px; }
856
  h1 { font-size: 1.8rem; }
857
  h2 { font-size: 1.5rem; margin-top: 30px; display: flex; align-items: center; gap: 8px; }
858
- h3 { font-size: 1.2rem; color: #388E3C; margin-top: 20px; }
859
- .section { margin-bottom: 30px; padding: 20px; background-color: #d7f7e0; border: 1px solid #c1e7cc; border-radius: 8px; }
860
  form { margin-bottom: 20px; }
861
- label { font-weight: 500; margin-top: 10px; display: block; color: #4f6a5a; font-size: 0.9rem;}
862
- input[type="text"], input[type="number"], input[type="password"], input[type="tel"], textarea, select { width: 100%; padding: 10px 12px; margin-top: 5px; border: 1px solid #9cdbb0; border-radius: 6px; font-size: 0.95rem; box-sizing: border-box; transition: border-color 0.3s ease; background-color: #fff; }
863
- input:focus, textarea:focus, select:focus { border-color: #4CAF50; outline: none; box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.1); }
864
  textarea { min-height: 80px; resize: vertical; }
865
- input[type="file"] { padding: 8px; background-color: #d7f7e0; cursor: pointer; border: 1px solid #9cdbb0;}
866
- input[type="file"]::file-selector-button { padding: 5px 10px; border-radius: 4px; background-color: #c1e7cc; border: 1px solid #9cdbb0; cursor: pointer; margin-right: 10px;}
867
  input[type="checkbox"] { margin-right: 5px; vertical-align: middle; }
868
  label.inline-label { display: inline-block; margin-top: 10px; font-weight: normal; }
869
- button, .button { padding: 10px 18px; border: none; border-radius: 6px; background-color: #4CAF50; 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;}
870
- button:hover, .button:hover { background-color: #388E3C; }
871
  button:active, .button:active { transform: scale(0.98); }
872
  button[type="submit"] { min-width: 120px; justify-content: center; }
873
- .delete-button { background-color: #f56565; }
874
- .delete-button:hover { background-color: #e53e3e; }
875
- .add-button { background-color: #4CAF50; }
876
- .add-button:hover { background-color: #388E3C; }
877
  .item-list { display: grid; gap: 20px; }
878
- .item { background: #fff; padding: 15px 20px; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.07); border: 1px solid #c1e7cc; }
879
- .item p { margin: 5px 0; font-size: 0.9rem; color: #4f6a5a; }
880
- .item strong { color: #1a362d; }
881
- .item .description { font-size: 0.85rem; color: #a0aec0; max-height: 60px; overflow: hidden; text-overflow: ellipsis; }
882
  .item-actions { margin-top: 15px; display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
883
- .item-actions button:not(.delete-button) { background-color: #4CAF50; }
884
- .item-actions button:not(.delete-button):hover { background-color: #388E3C; }
885
- .edit-form-container { margin-top: 15px; padding: 20px; background: #d7f7e0; border: 1px dashed #9cdbb0; border-radius: 6px; display: none; }
886
- details { background-color: #d7f7e0; border: 1px solid #c1e7cc; border-radius: 8px; margin-bottom: 20px; }
887
- details > summary { cursor: pointer; font-weight: 600; color: #388E3C; display: block; padding: 15px; border-bottom: 1px solid #9cdbb0; list-style: none; position: relative; }
888
- details > summary::after { content: '\\f078'; font-family: 'Font Awesome 6 Free'; font-weight: 900; position: absolute; right: 20px; top: 50%; transform: translateY(-50%); transition: transform 0.2s ease; color: #4CAF50; }
889
  details[open] > summary::after { transform: translateY(-50%) rotate(180deg); }
890
- details[open] > summary { border-bottom: 1px solid #9cdbb0; }
891
  details .form-content { padding: 20px; }
892
  .color-input-group { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
893
  .color-input-group input { flex-grow: 1; margin: 0; }
894
- .remove-color-btn { background-color: #f56565; padding: 6px 10px; font-size: 0.8rem; margin-top: 0; line-height: 1; }
895
- .remove-color-btn:hover { background-color: #e53e3e; }
896
- .add-color-btn { background-color: #a7f3d0; color: #388E3C; }
897
- .add-color-btn:hover { background-color: #c1e7cc; }
898
- .photo-preview img { max-width: 70px; max-height: 70px; border-radius: 5px; margin: 5px 5px 0 0; border: 1px solid #9cdbb0; object-fit: cover;}
899
  .sync-buttons { display: flex; gap: 10px; margin-bottom: 20px; flex-wrap: wrap; }
900
- .download-hf-button { background-color: #a0aec0; }
901
- .download-hf-button:hover { background-color: #718096; }
902
  .flex-container { display: flex; flex-wrap: wrap; gap: 20px; }
903
  .flex-item { flex: 1; min-width: 350px; }
904
  .message { padding: 10px 15px; border-radius: 6px; margin-bottom: 15px; font-size: 0.9rem;}
905
- .message.success { background-color: #f0fff4; color: #155724; border: 1px solid #c3e6cb;}
906
- .message.error { background-color: #fff5f5; color: #721c24; border: 1px solid #f5c6cb;}
907
- .message.warning { background-color: #fffaf0; color: #856404; border: 1px solid #ffeeba; }
908
  .status-indicator { display: inline-block; padding: 3px 8px; border-radius: 12px; font-size: 0.8rem; font-weight: 500; margin-left: 10px; vertical-align: middle; }
909
- .status-indicator.in-stock { background-color: #c6f6d5; color: #2f855a; }
910
- .status-indicator.out-of-stock { background-color: #fed7d7; color: #c53030; }
911
- .status-indicator.top-product { background-color: #feebc8; color: #9c4221; margin-left: 5px;}
912
  </style>
913
  </head>
914
  <body>
915
  <div class="container">
916
  <div class="header">
917
  <div class="logo-title-container" style="display: flex; align-items: center; gap: 15px;">
918
- <img src="https://upload.wikimedia.org/wikipedia/commons/thumb/a/ab/Apple-logo.png/2560px-Apple-logo.png" alt="Meka Shop Logo" style="height: 40px; width: auto; border-radius: 4px;">
919
  <h1><i class="fas fa-tools"></i> Админ-панель Meka Shop</h1>
920
  </div>
921
- <a href="{{ url_for('catalog') }}" class="button" style="background-color: #388E3C;"><i class="fas fa-store"></i> Перейти в каталог</a>
922
  </div>
 
923
  {% with messages = get_flashed_messages(with_categories=true) %}
924
  {% if messages %}
925
  {% for category, message in messages %}
@@ -927,6 +982,7 @@ ADMIN_TEMPLATE = '''
927
  {% endfor %}
928
  {% endif %}
929
  {% endwith %}
 
930
  <div class="section">
931
  <h2><i class="fas fa-sync-alt"></i> Синхронизация с Датацентром</h2>
932
  <div class="sync-buttons">
@@ -937,8 +993,9 @@ ADMIN_TEMPLATE = '''
937
  <button type="submit" class="button download-hf-button" title="Скачать файлы (перезапишет локальные)"><i class="fas fa-download"></i> Скачать БД</button>
938
  </form>
939
  </div>
940
- <p style="font-size: 0.85rem; color: #4f6a5a;">Резервное копирование происходит автоматически каждые 30 минут, а также после каждого сохранения данных. Используйте эти кнопки для немедленной синхронизации.</p>
941
  </div>
 
942
  <div class="flex-container">
943
  <div class="flex-item">
944
  <div class="section">
@@ -954,6 +1011,7 @@ ADMIN_TEMPLATE = '''
954
  </form>
955
  </div>
956
  </details>
 
957
  <h3>Существующие категории:</h3>
958
  {% if categories %}
959
  <div class="item-list">
@@ -973,6 +1031,7 @@ ADMIN_TEMPLATE = '''
973
  {% endif %}
974
  </div>
975
  </div>
 
976
  <div class="flex-item">
977
  <div class="section">
978
  <h2><i class="fas fa-info-circle"></i> Информация</h2>
@@ -981,6 +1040,7 @@ ADMIN_TEMPLATE = '''
981
  </div>
982
  </div>
983
  </div>
 
984
  <div class="section">
985
  <h2><i class="fas fa-box-open"></i> Управление товарами</h2>
986
  <details>
@@ -1025,6 +1085,7 @@ ADMIN_TEMPLATE = '''
1025
  </form>
1026
  </div>
1027
  </details>
 
1028
  <h3>Список товаров:</h3>
1029
  {% if products %}
1030
  <div class="item-list">
@@ -1041,7 +1102,7 @@ ADMIN_TEMPLATE = '''
1041
  {% endif %}
1042
  </div>
1043
  <div style="flex-grow: 1;">
1044
- <h3 style="margin-top: 0; margin-bottom: 5px; color: #1a362d;">
1045
  {{ product['name'] }}
1046
  {% if product.get('in_stock', True) %}
1047
  <span class="status-indicator in-stock">В наличии</span>
@@ -1058,10 +1119,11 @@ ADMIN_TEMPLATE = '''
1058
  {% set colors = product.get('colors', []) %}
1059
  <p><strong>Цвета/Вар-ты:</strong> {{ colors|select('ne', '')|join(', ') if colors|select('ne', '')|list|length > 0 else 'Нет' }}</p>
1060
  {% if product.get('photos') and product['photos']|length > 1 %}
1061
- <p style="font-size: 0.8rem; color: #4f6a5a;">(Всего фото: {{ product['photos']|length }})</p>
1062
  {% endif %}
1063
  </div>
1064
  </div>
 
1065
  <div class="item-actions">
1066
  <button type="button" class="button" onclick="toggleEditForm('edit-form-{{ loop.index0 }}')"><i class="fas fa-edit"></i> Редактировать</button>
1067
  <form method="POST" style="margin:0;" onsubmit="return confirm('Вы уверены, что хотите удалить товар \'{{ product['name'] }}\'?');">
@@ -1070,6 +1132,7 @@ ADMIN_TEMPLATE = '''
1070
  <button type="submit" class="delete-button"><i class="fas fa-trash-alt"></i> Удалить</button>
1071
  </form>
1072
  </div>
 
1073
  <div id="edit-form-{{ loop.index0 }}" class="edit-form-container">
1074
  <h4><i class="fas fa-edit"></i> Редактирование: {{ product['name'] }}</h4>
1075
  <form method="POST" enctype="multipart/form-data">
@@ -1138,7 +1201,9 @@ ADMIN_TEMPLATE = '''
1138
  <p>Товаров пока нет.</p>
1139
  {% endif %}
1140
  </div>
 
1141
  </div>
 
1142
  <script>
1143
  function toggleEditForm(formId) {
1144
  const formContainer = document.getElementById(formId);
@@ -1146,6 +1211,7 @@ ADMIN_TEMPLATE = '''
1146
  formContainer.style.display = formContainer.style.display === 'none' || formContainer.style.display === '' ? 'block' : 'none';
1147
  }
1148
  }
 
1149
  function addColorInput(containerId) {
1150
  const container = document.getElementById(containerId);
1151
  if (container) {
@@ -1162,6 +1228,7 @@ ADMIN_TEMPLATE = '''
1162
  }
1163
  }
1164
  }
 
1165
  function removeColorInput(button) {
1166
  const group = button.closest('.color-input-group');
1167
  if (group) {
@@ -1184,13 +1251,17 @@ ADMIN_TEMPLATE = '''
1184
  </body>
1185
  </html>
1186
  '''
 
 
1187
  @app.route('/')
1188
  def catalog():
1189
  data = load_data()
1190
  all_products = data.get('products', [])
1191
  categories = sorted(data.get('categories', []))
 
1192
  products_in_stock = [p for p in all_products if p.get('in_stock', True)]
1193
  products_sorted = sorted(products_in_stock, key=lambda p: (not p.get('is_top', False), p.get('name', '').lower()))
 
1194
  return render_template_string(
1195
  CATALOG_TEMPLATE,
1196
  products=products_sorted,
@@ -1199,30 +1270,37 @@ def catalog():
1199
  store_address=STORE_ADDRESS,
1200
  currency_code=CURRENCY_CODE
1201
  )
 
1202
  @app.route('/product/<int:index>')
1203
  def product_detail(index):
1204
  data = load_data()
1205
  all_products = data.get('products', [])
1206
  products_in_stock = [p for p in all_products if p.get('in_stock', True)]
1207
  products_sorted = sorted(products_in_stock, key=lambda p: (not p.get('is_top', False), p.get('name', '').lower()))
 
1208
  try:
1209
  product = products_sorted[index]
1210
  except IndexError:
1211
  logging.warning(f"Attempted access to non-existent or out-of-stock product with index {index}")
1212
  return "Товар не найден или отсутствует в наличии.", 404
 
1213
  return render_template_string(
1214
  PRODUCT_DETAIL_TEMPLATE,
1215
  product=product,
1216
  repo_id=REPO_ID,
1217
  currency_code=CURRENCY_CODE
1218
  )
 
1219
  @app.route('/create_order', methods=['POST'])
1220
  def create_order():
1221
  order_data = request.get_json()
 
1222
  if not order_data or 'cart' not in order_data or not order_data['cart']:
1223
  logging.warning("Create order request missing cart data or cart is empty.")
1224
  return jsonify({"error": "Корзина пуста или не передана."}), 400
 
1225
  cart_items = order_data['cart']
 
1226
  total_price = 0
1227
  processed_cart = []
1228
  for item in cart_items:
@@ -1246,8 +1324,10 @@ def create_order():
1246
  except (ValueError, TypeError) as e:
1247
  logging.error(f"Invalid price/quantity in cart item: {item}. Error: {e}")
1248
  return jsonify({"error": "Неверная цена или количество в товаре."}), 400
 
1249
  order_id = f"{datetime.now().strftime('%Y%m%d%H%M%S')}-{uuid.uuid4().hex[:6]}"
1250
  order_timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
 
1251
  new_order = {
1252
  "id": order_id,
1253
  "created_at": order_timestamp,
@@ -1256,30 +1336,38 @@ def create_order():
1256
  "user_info": None,
1257
  "status": "new"
1258
  }
 
1259
  try:
1260
  data = load_data()
1261
  if 'orders' not in data or not isinstance(data.get('orders'), dict):
1262
  data['orders'] = {}
 
1263
  data['orders'][order_id] = new_order
1264
  save_data(data)
1265
  logging.info(f"Order {order_id} created successfully (anonymously).")
1266
  return jsonify({"order_id": order_id}), 201
 
1267
  except Exception as e:
1268
  logging.error(f"Failed to save order {order_id}: {e}", exc_info=True)
1269
  return jsonify({"error": "Ошибка серве��а при сохранении заказа."}), 500
 
 
1270
  @app.route('/order/<order_id>')
1271
  def view_order(order_id):
1272
  data = load_data()
1273
  order = data.get('orders', {}).get(order_id)
 
1274
  if order:
1275
  logging.info(f"Displaying order {order_id}")
1276
  else:
1277
  logging.warning(f"Order {order_id} not found.")
 
1278
  return render_template_string(ORDER_TEMPLATE,
1279
  order=order,
1280
  repo_id=REPO_ID,
1281
- currency_code=CURRENCY_CODE,
1282
- whatsapp_number=WHATSAPP_NUMBER)
 
1283
  @app.route('/admin', methods=['GET', 'POST'])
1284
  def admin():
1285
  data = load_data()
@@ -1287,9 +1375,11 @@ def admin():
1287
  categories = data.get('categories', [])
1288
  if 'orders' not in data or not isinstance(data.get('orders'), dict):
1289
  data['orders'] = {}
 
1290
  if request.method == 'POST':
1291
  action = request.form.get('action')
1292
  logging.info(f"Admin action received: {action}")
 
1293
  try:
1294
  if action == 'add_category':
1295
  category_name = request.form.get('category_name', '').strip()
@@ -1305,6 +1395,7 @@ def admin():
1305
  else:
1306
  logging.warning(f"Category '{category_name}' already exists.")
1307
  flash(f"Категория '{category_name}' уже существует.", 'error')
 
1308
  elif action == 'delete_category':
1309
  category_to_delete = request.form.get('category_name')
1310
  if category_to_delete and category_to_delete in categories:
@@ -1322,6 +1413,7 @@ def admin():
1322
  else:
1323
  logging.warning(f"Attempted to delete non-existent or empty category: {category_to_delete}")
1324
  flash(f"Не удалось удалить категорию '{category_to_delete}'.", 'error')
 
1325
  elif action == 'add_product':
1326
  name = request.form.get('name', '').strip()
1327
  price_str = request.form.get('price', '').replace(',', '.')
@@ -1331,15 +1423,18 @@ def admin():
1331
  colors = [c.strip() for c in request.form.getlist('colors') if c.strip()]
1332
  in_stock = 'in_stock' in request.form
1333
  is_top = 'is_top' in request.form
 
1334
  if not name or not price_str:
1335
  flash("Название и цена товара обязательны.", 'error')
1336
  return redirect(url_for('admin'))
 
1337
  try:
1338
  price = round(float(price_str), 2)
1339
  if price < 0: price = 0
1340
  except ValueError:
1341
  flash("Неверный формат цены.", 'error')
1342
  return redirect(url_for('admin'))
 
1343
  photos_list = []
1344
  if photos_files and HF_TOKEN_WRITE:
1345
  uploads_dir = 'uploads_temp'
@@ -1359,6 +1454,7 @@ def admin():
1359
  logging.warning(f"Skipping non-image file upload: {photo.filename}")
1360
  flash(f"Файл {photo.filename} не является изображением и был пропущен.", "warning")
1361
  continue
 
1362
  safe_name = secure_filename(name.replace(' ', '_'))[:50]
1363
  photo_filename = f"{safe_name}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{ext}"
1364
  temp_path = os.path.join(uploads_dir, photo_filename)
@@ -1391,6 +1487,8 @@ def admin():
1391
  logging.warning(f"Could not remove temporary upload directory {uploads_dir}: {e}")
1392
  elif not HF_TOKEN_WRITE and photos_files and any(f.filename for f in photos_files):
1393
  flash("HF_TOKEN (write) не настроен. Фотографии не были загружены.", "warning")
 
 
1394
  new_product = {
1395
  'name': name, 'price': price, 'description': description,
1396
  'category': category if category in categories else 'Без категории',
@@ -1402,21 +1500,25 @@ def admin():
1402
  save_data(data)
1403
  logging.info(f"Product '{name}' added.")
1404
  flash(f"Товар '{name}' успешно добавлен.", 'success')
 
1405
  elif action == 'edit_product':
1406
  index_str = request.form.get('index')
1407
  if index_str is None:
1408
  flash("Ошибка редактирования: индекс товара не передан.", 'error')
1409
  return redirect(url_for('admin'))
 
1410
  try:
1411
  index = int(index_str)
1412
  if not (0 <= index < len(products)):
1413
  raise IndexError("Product index out of range")
1414
  product_to_edit = products[index]
1415
  original_name = product_to_edit.get('name', 'N/A')
 
1416
  except (ValueError, IndexError):
1417
  flash(f"Ошибка редактирования: неверный индекс товара '{index_str}'.", 'error')
1418
  logging.error(f"Invalid index '{index_str}' for editing. Product list length: {len(products)}")
1419
  return redirect(url_for('admin'))
 
1420
  product_to_edit['name'] = request.form.get('name', product_to_edit['name']).strip()
1421
  price_str = request.form.get('price', str(product_to_edit['price'])).replace(',', '.')
1422
  product_to_edit['description'] = request.form.get('description', product_to_edit.get('description', '')).strip()
@@ -1425,6 +1527,7 @@ def admin():
1425
  product_to_edit['colors'] = [c.strip() for c in request.form.getlist('colors') if c.strip()]
1426
  product_to_edit['in_stock'] = 'in_stock' in request.form
1427
  product_to_edit['is_top'] = 'is_top' in request.form
 
1428
  try:
1429
  price = round(float(price_str), 2)
1430
  if price < 0: price = 0
@@ -1432,6 +1535,7 @@ def admin():
1432
  except ValueError:
1433
  logging.warning(f"Invalid price format '{price_str}' during edit of product {original_name}. Price not changed.")
1434
  flash(f"Неверный формат цены для товара '{original_name}'. Цена не изменена.", 'warning')
 
1435
  photos_files = request.files.getlist('photos')
1436
  if photos_files and any(f.filename for f in photos_files) and HF_TOKEN_WRITE:
1437
  uploads_dir = 'uploads_temp'
@@ -1453,6 +1557,7 @@ def admin():
1453
  logging.warning(f"Skipping non-image file upload during edit: {photo.filename}")
1454
  flash(f"Файл {photo.filename} не является изображением и был пропущен.", "warning")
1455
  continue
 
1456
  safe_name = secure_filename(product_to_edit['name'].replace(' ', '_'))[:50]
1457
  photo_filename = f"{safe_name}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{ext}"
1458
  temp_path = os.path.join(uploads_dir, photo_filename)
@@ -1476,6 +1581,7 @@ def admin():
1476
  os.rmdir(uploads_dir)
1477
  except OSError as e:
1478
  logging.warning(f"Could not remove temporary upload directory {uploads_dir}: {e}")
 
1479
  if new_photos_list:
1480
  logging.info(f"New photo list for product {product_to_edit['name']} generated.")
1481
  old_photos = product_to_edit.get('photos', [])
@@ -1499,11 +1605,13 @@ def admin():
1499
  flash("Не удалось загрузить новые фотографии (возможно, неверный формат).", "error")
1500
  elif not HF_TOKEN_WRITE and photos_files and any(f.filename for f in photos_files):
1501
  flash("HF_TOKEN (write) не настроен. Фотографии не были обновлены.", "warning")
 
1502
  products[index] = product_to_edit
1503
  data['products'] = products
1504
  save_data(data)
1505
  logging.info(f"Product '{original_name}' (index {index}) updated to '{product_to_edit['name']}'.")
1506
  flash(f"Товар '{product_to_edit['name']}' успешно обновлен.", 'success')
 
1507
  elif action == 'delete_product':
1508
  index_str = request.form.get('index')
1509
  if index_str is None:
@@ -1514,6 +1622,7 @@ def admin():
1514
  if not (0 <= index < len(products)): raise IndexError("Product index out of range")
1515
  deleted_product = products.pop(index)
1516
  product_name = deleted_product.get('name', 'N/A')
 
1517
  photos_to_delete = deleted_product.get('photos', [])
1518
  if photos_to_delete and HF_TOKEN_WRITE:
1519
  logging.info(f"Attempting to delete photos for product '{product_name}' from HF: {photos_to_delete}")
@@ -1533,6 +1642,8 @@ def admin():
1533
  elif photos_to_delete and not HF_TOKEN_WRITE:
1534
  logging.warning(f"HF_TOKEN (write) not set. Cannot delete photos {photos_to_delete} for deleted product '{product_name}'.")
1535
  flash(f"Товар '{product_name}' удален локально, но фото не удалены с сервера (токен не задан).", "warning")
 
 
1536
  data['products'] = products
1537
  save_data(data)
1538
  logging.info(f"Product '{product_name}' (original index {index}) deleted.")
@@ -1540,17 +1651,22 @@ def admin():
1540
  except (ValueError, IndexError):
1541
  flash(f"Ошибка удаления: неверный индекс товара '{index_str}'.", 'error')
1542
  logging.error(f"Invalid index '{index_str}' for deletion. Product list length: {len(products)}")
 
1543
  else:
1544
  logging.warning(f"Received unknown admin action: {action}")
1545
  flash(f"Неизвестное действие: {action}", 'warning')
 
1546
  return redirect(url_for('admin'))
 
1547
  except Exception as e:
1548
- logging.error(f"Error processing admin action '{action}': {e}", exc_info=True)
1549
  flash(f"Произошла внутренняя ошибка при выполнении действия '{action}'. Подробности в логе сервера.", 'error')
1550
  return redirect(url_for('admin'))
 
1551
  current_data = load_data()
1552
  display_products = sorted(current_data.get('products', []), key=lambda p: p.get('name', '').lower())
1553
  display_categories = sorted(current_data.get('categories', []))
 
1554
  return render_template_string(
1555
  ADMIN_TEMPLATE,
1556
  products=display_products,
@@ -1558,6 +1674,7 @@ def admin():
1558
  repo_id=REPO_ID,
1559
  currency_code=CURRENCY_CODE
1560
  )
 
1561
  @app.route('/force_upload', methods=['POST'])
1562
  def force_upload():
1563
  logging.info("Forcing upload to Hugging Face...")
@@ -1568,6 +1685,7 @@ def force_upload():
1568
  logging.error(f"Error during forced upload: {e}", exc_info=True)
1569
  flash(f"Ошибка при загрузке на Hugging Face: {e}", 'error')
1570
  return redirect(url_for('admin'))
 
1571
  @app.route('/force_download', methods=['POST'])
1572
  def force_download():
1573
  logging.info("Forcing download from Hugging Face...")
@@ -1581,17 +1699,21 @@ def force_download():
1581
  logging.error(f"Error during forced download: {e}", exc_info=True)
1582
  flash(f"Ошибка при скачивании с Hugging Face: {e}", 'error')
1583
  return redirect(url_for('admin'))
 
 
1584
  if __name__ == '__main__':
1585
  logging.info("Application starting up. Performing initial data load/download...")
1586
  download_db_from_hf()
1587
  load_data()
1588
  logging.info("Initial data load complete.")
 
1589
  if HF_TOKEN_WRITE:
1590
  backup_thread = threading.Thread(target=periodic_backup, daemon=True)
1591
  backup_thread.start()
1592
  logging.info("Periodic backup thread started.")
1593
  else:
1594
  logging.warning("Periodic backup will NOT run (HF_TOKEN for writing not set).")
 
1595
  port = int(os.environ.get('PORT', 7860))
1596
  logging.info(f"Starting Flask app on host 0.0.0.0 and port {port}")
1597
  app.run(debug=False, host='0.0.0.0', port=port)
 
12
  from dotenv import load_dotenv
13
  import requests
14
  import uuid
15
+
16
  load_dotenv()
17
+
18
  app = Flask(__name__)
19
+ app.secret_key = 'your_unique_secret_key_meka_shop_12345_no_login'
20
  DATA_FILE = 'data.json'
21
+
22
  SYNC_FILES = [DATA_FILE]
23
+
24
  REPO_ID = "Kgshop/nizhbel"
25
  HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
26
  HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
27
+
28
  STORE_ADDRESS = "Рынок Кербент, 6 ряд , 3 контейнер / 5 ряд 25 контейнер "
29
+
30
  CURRENCY_CODE = 'KGS'
31
  CURRENCY_NAME = 'Кыргызский сом'
32
+
33
  DOWNLOAD_RETRIES = 3
34
  DOWNLOAD_DELAY = 5
35
+
36
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
37
+
38
  def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY):
39
  if not HF_TOKEN_READ and not HF_TOKEN_WRITE:
40
  logging.warning("HF_TOKEN_READ/HF_TOKEN_WRITE not set. Download might fail for private repos.")
41
+
42
  token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE
43
+
44
  files_to_download = [specific_file] if specific_file else SYNC_FILES
45
  logging.info(f"Attempting download for {files_to_download} from {REPO_ID}...")
46
  all_successful = True
47
+
48
  for file_name in files_to_download:
49
  success = False
50
  for attempt in range(retries + 1):
 
85
  logging.error(f"Network error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...")
86
  except Exception as e:
87
  logging.error(f"Unexpected error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...", exc_info=True)
88
+
89
  if attempt < retries:
90
  time.sleep(delay)
91
+
92
  if not success:
93
  logging.error(f"Failed to download {file_name} after {retries + 1} attempts.")
94
  all_successful = False
95
+
96
  logging.info(f"Download process finished. Overall success: {all_successful}")
97
  return all_successful
98
+
99
  def upload_db_to_hf(specific_file=None):
100
  if not HF_TOKEN_WRITE:
101
  logging.warning("HF_TOKEN (for writing) not set. Skipping upload to Hugging Face.")
 
104
  api = HfApi()
105
  files_to_upload = [specific_file] if specific_file else SYNC_FILES
106
  logging.info(f"Starting upload of {files_to_upload} to HF repo {REPO_ID}...")
107
+
108
  for file_name in files_to_upload:
109
  if os.path.exists(file_name):
110
  try:
 
124
  logging.info("Finished uploading files to HF.")
125
  except Exception as e:
126
  logging.error(f"General error during Hugging Face upload initialization or process: {e}", exc_info=True)
127
+
128
  def periodic_backup():
129
  backup_interval = 1800
130
  logging.info(f"Setting up periodic backup every {backup_interval} seconds.")
 
133
  logging.info("Starting periodic backup...")
134
  upload_db_to_hf()
135
  logging.info("Periodic backup finished.")
136
+
137
  def load_data():
138
  default_data = {'products': [], 'categories': [], 'orders': {}}
139
  try:
 
151
  logging.warning(f"Local file {DATA_FILE} not found. Attempting download from HF.")
152
  except json.JSONDecodeError:
153
  logging.error(f"Error decoding JSON in local {DATA_FILE}. File might be corrupt. Attempting download.")
154
+
155
  if download_db_from_hf(specific_file=DATA_FILE):
156
  try:
157
  with open(DATA_FILE, 'r', encoding='utf-8') as file:
 
183
  except Exception as create_e:
184
  logging.error(f"Failed to create empty local file {DATA_FILE}: {create_e}")
185
  return default_data
186
+
187
+
188
  def save_data(data):
189
  try:
190
  if not isinstance(data, dict):
 
193
  if 'products' not in data: data['products'] = []
194
  if 'categories' not in data: data['categories'] = []
195
  if 'orders' not in data: data['orders'] = {}
196
+
197
  with open(DATA_FILE, 'w', encoding='utf-8') as file:
198
  json.dump(data, file, ensure_ascii=False, indent=4)
199
  logging.info(f"Data successfully saved to {DATA_FILE}")
200
  upload_db_to_hf(specific_file=DATA_FILE)
201
  except Exception as e:
202
  logging.error(f"Error saving data to {DATA_FILE}: {e}", exc_info=True)
203
+
204
+
205
  CATALOG_TEMPLATE = '''
206
  <!DOCTYPE html>
207
  <html lang="ru">
 
214
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.css">
215
  <style>
216
  * { margin: 0; padding: 0; box-sizing: border-box; }
217
+ body { font-family: 'Poppins', sans-serif; background: #ffffff; color: #333333; line-height: 1.6; }
 
218
  .container { max-width: 1300px; margin: 0 auto; padding: 20px; }
219
+ .header { display: flex; justify-content: space-between; align-items: center; padding: 15px 0; border-bottom: 1px solid #e0e0e0; }
220
+ .header h1 { font-size: 1.8rem; font-weight: 600; color: #E91E63; }
221
+ .store-address { padding: 15px; text-align: center; background-color: #f9f9f9; margin: 20px 0; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.03); font-size: 1rem; color: #666; }
 
 
 
 
 
 
222
  .filters-container { margin: 20px 0; display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }
223
  .search-container { margin: 20px 0; text-align: center; }
224
+ #search-input { width: 90%; max-width: 600px; padding: 12px 18px; font-size: 1rem; border: 1px solid #e0e0e0; border-radius: 25px; outline: none; box-shadow: 0 2px 5px rgba(0,0,0,0.03); transition: all 0.3s ease; }
225
+ #search-input:focus { border-color: #E91E63; box-shadow: 0 0 0 3px rgba(233, 30, 99, 0.15); }
226
+ .category-filter { padding: 8px 16px; border: 1px solid #e0e0e0; 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: #C2185B; }
227
+ .category-filter.active, .category-filter:hover { background-color: #E91E63; color: white; border-color: #E91E63; box-shadow: 0 2px 10px rgba(233, 30, 99, 0.2); }
 
 
 
 
228
  .products-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 20px; padding: 10px; }
229
  @media (min-width: 600px) { .products-grid { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); } }
230
  @media (min-width: 900px) { .products-grid { grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); } }
231
+
232
+ .product { background: #fff; border-radius: 15px; padding: 0; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05); 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 #f0f0f0;}
233
+ .product:hover { transform: translateY(-5px) scale(1.02); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1); }
 
234
  .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; }
235
  .product-image img { max-width: 100%; max-height: 100%; object-fit: contain; transition: transform 0.3s ease; }
236
  .product-info { padding: 15px; flex-grow: 1; display: flex; flex-direction: column; justify-content: center; }
237
+ .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; }
238
+ .product-price { font-size: 1.2rem; color: #E91E63; font-weight: 700; text-align: center; margin: 5px 0; }
239
+ .product-description { font-size: 0.85rem; color: #666; text-align: center; margin-bottom: 15px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
 
 
 
240
  .product-actions { padding: 0 15px 15px 15px; display: flex; flex-direction: column; gap: 8px; }
241
+ .product-button { display: block; width: 100%; padding: 10px; border: none; border-radius: 8px; background-color: #F06292; 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; }
242
+ .product-button:hover { background-color: #E91E63; box-shadow: 0 4px 15px rgba(233, 30, 99, 0.3); transform: translateY(-2px); }
243
  .product-button i { margin-right: 5px; }
244
+ .add-to-cart { background-color: #E91E63; }
245
+ .add-to-cart:hover { background-color: #C2185B; box-shadow: 0 4px 15px rgba(194, 24, 91, 0.4); }
246
+ #cart-button { position: fixed; bottom: 25px; right: 25px; background-color: #E91E63; 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(233, 30, 99, 0.4); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); z-index: 1000; }
247
+ #cart-button:hover { background-color: #C2185B; box-shadow: 0 6px 20px rgba(194, 24, 91, 0.5); }
248
  #cart-button .fa-shopping-cart { margin-right: 0; }
249
+ #cart-button span { position: absolute; top: -5px; right: -5px; background-color: #C2185B; color: white; border-radius: 50%; padding: 2px 6px; font-size: 0.7rem; font-weight: bold; }
250
+ .modal { display: none; position: fixed; z-index: 1001; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); backdrop-filter: blur(3px); overflow-y: auto; }
251
+ .modal-content { background: #ffffff; margin: 5% auto; padding: 25px; border-radius: 15px; width: 90%; max-width: 700px; box-shadow: 0 10px 30px rgba(0,0,0,0.1); animation: slideIn 0.3s ease-out; position: relative; }
 
252
  @keyframes slideIn { from { transform: translateY(-30px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
253
  .close { position: absolute; top: 15px; right: 15px; font-size: 1.8rem; color: #aaa; cursor: pointer; transition: color 0.3s; line-height: 1; }
254
+ .close:hover { color: #666; }
255
+ .modal-content h2 { margin-top: 0; margin-bottom: 20px; color: #E91E63; display: flex; align-items: center; gap: 10px;}
256
+ .cart-item { display: grid; grid-template-columns: auto 1fr auto auto; gap: 15px; align-items: center; padding: 15px 0; border-bottom: 1px solid #e0e0e0; }
 
 
 
 
257
  .cart-item:last-child { border-bottom: none; }
258
+ .cart-item img { width: 60px; height: 60px; object-fit: contain; border-radius: 8px; background-color: #fff; padding: 5px; grid-column: 1; border: 1px solid #e0e0e0;}
259
  .cart-item-details { grid-column: 2; }
260
+ .cart-item-details strong { display: block; margin-bottom: 5px; font-size: 1rem; color: #333;}
261
+ .cart-item-price { font-size: 0.9rem; color: #666; }
262
+ .cart-item-total { font-weight: bold; text-align: right; grid-column: 3; font-size: 1rem; color: #C2185B;}
263
+ .cart-item-remove { grid-column: 4; background:none; border:none; color:#dc3545; cursor:pointer; font-size: 1.3em; padding: 5px; line-height: 1; }
264
+ .cart-item-remove:hover { color: #c82333; }
265
+ .quantity-input, .color-select { width: 100%; max-width: 180px; padding: 10px; border: 1px solid #e0e0e0; border-radius: 8px; font-size: 1rem; margin: 10px 0; box-sizing: border-box; }
266
+ .quantity-input:focus, .color-select:focus { border-color: #F06292; outline: none; box-shadow: 0 0 0 2px rgba(240, 98, 146, 0.1); }
267
+ .cart-summary { margin-top: 20px; text-align: right; border-top: 1px solid #e0e0e0; padding-top: 15px; }
268
+ .cart-summary strong { font-size: 1.2rem; color: #E91E63;}
 
 
269
  .cart-actions { margin-top: 25px; display: flex; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
270
  .cart-actions .product-button { width: auto; flex-grow: 1; }
271
+ .clear-cart { background-color: #6c757d; }
272
+ .clear-cart:hover { background-color: #5a6268; box-shadow: 0 4px 15px rgba(90, 98, 104, 0.4); }
273
+ .formulate-order-button { background-color: #E91E63; }
274
+ .formulate-order-button:hover { background-color: #C2185B; box-shadow: 0 4px 15px rgba(194, 24, 91, 0.4); }
275
+ .notification { position: fixed; bottom: 80px; left: 50%; transform: translateX(-50%); background-color: #E91E63; 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;}
276
  .notification.show { opacity: 1;}
277
+ .no-results-message { grid-column: 1 / -1; text-align: center; padding: 40px; font-size: 1.1rem; color: #999; }
 
278
  .top-product-indicator { position: absolute; top: 8px; right: 8px; background-color: rgba(255, 215, 0, 0.8); color: #333; padding: 2px 6px; font-size: 0.7rem; border-radius: 4px; font-weight: bold; z-index: 10; backdrop-filter: blur(2px); }
279
  .product { position: relative; }
280
  </style>
 
283
  <div class="container">
284
  <div class="header">
285
  <div class="logo-title-container" style="display: flex; align-items: center; gap: 15px;">
 
286
  <h1>Meka Shop</h1>
287
  </div>
 
 
 
288
  </div>
289
+
290
  <div class="store-address">Наш адрес: {{ store_address }}</div>
291
+
292
  <div class="filters-container">
293
  <button class="category-filter active" data-category="all">Все категории</button>
294
  {% for category in categories %}
295
  <button class="category-filter" data-category="{{ category }}">{{ category }}</button>
296
  {% endfor %}
297
  </div>
298
+
299
  <div class="search-container">
300
  <input type="text" id="search-input" placeholder="Поиск по названию или описанию...">
301
  </div>
302
+
303
  <div class="products-grid" id="products-grid">
304
  {% for product in products %}
305
  <div class="product"
 
336
  {% endif %}
337
  </div>
338
  </div>
339
+
340
  <div id="productModal" class="modal">
341
  <div class="modal-content">
342
  <span class="close" onclick="closeModal('productModal')" aria-label="Закрыть">×</span>
343
  <div id="modalContent">Загрузка...</div>
344
  </div>
345
  </div>
346
+
347
  <div id="quantityModal" class="modal">
348
  <div class="modal-content">
349
  <span class="close" onclick="closeModal('quantityModal')" aria-label="Закрыть">×</span>
 
355
  <button class="product-button add-to-cart" onclick="confirmAddToCart()"><i class="fas fa-check"></i> Добавить в корзину</button>
356
  </div>
357
  </div>
358
+
359
  <div id="cartModal" class="modal">
360
  <div class="modal-content">
361
  <span class="close" onclick="closeModal('cartModal')" aria-label="Закрыть">×</span>
 
374
  </div>
375
  </div>
376
  </div>
377
+
378
  <button id="cart-button" onclick="openCartModal()" aria-label="Открыть корзину">
379
  <i class="fas fa-shopping-cart"></i>
380
  <span id="cart-count">0</span>
381
  </button>
382
+
383
  <div id="notification-placeholder"></div>
384
+
385
  <script src="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.js"></script>
386
  <script>
387
  const products = {{ products|tojson }};
388
  const repoId = '{{ repo_id }}';
389
  const currencyCode = '{{ currency_code }}';
390
  let selectedProductIndex = null;
391
+ let cart = JSON.parse(localStorage.getItem('mekaCart') || '[]');
392
+
393
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
394
  function openModal(index) {
395
  loadProductDetails(index);
396
  const modal = document.getElementById('productModal');
 
399
  document.body.style.overflow = 'hidden';
400
  }
401
  }
402
+
403
  function closeModal(modalId) {
404
  const modal = document.getElementById(modalId);
405
  if (modal) {
 
410
  document.body.style.overflow = 'auto';
411
  }
412
  }
413
+
414
  function loadProductDetails(index) {
415
  const modalContent = document.getElementById('modalContent');
416
  if (!modalContent) return;
 
426
  })
427
  .catch(error => {
428
  console.error('Ошибка загрузки деталей продукта:', error);
429
+ modalContent.innerHTML = `<p style="color: #dc3545; text-align:center; padding: 40px;">Не удалось загрузить информацию о товаре. ${error.message}</p>`;
430
  });
431
  }
432
+
433
  function initializeSwiper() {
434
  const swiperContainer = document.querySelector('#productModal .swiper-container');
435
  if (swiperContainer) {
 
445
  });
446
  }
447
  }
448
+
449
  function openQuantityModal(index) {
450
  selectedProductIndex = index;
451
  const product = products[index];
 
454
  alert("Ошибка: товар не найден.");
455
  return;
456
  }
457
+
458
  const colorSelect = document.getElementById('colorSelect');
459
  const colorLabel = document.querySelector('label[for="colorSelect"]');
460
  colorSelect.innerHTML = '';
461
+
462
  const validColors = product.colors ? product.colors.filter(c => c && c.trim() !== "") : [];
463
+
464
  if (validColors.length > 0) {
465
  validColors.forEach(color => {
466
  const option = document.createElement('option');
 
474
  colorSelect.style.display = 'none';
475
  if(colorLabel) colorLabel.style.display = 'none';
476
  }
477
+
478
  document.getElementById('quantityInput').value = 1;
479
  const modal = document.getElementById('quantityModal');
480
  if(modal) {
 
482
  document.body.style.overflow = 'hidden';
483
  }
484
  }
485
+
486
  function confirmAddToCart() {
487
  if (selectedProductIndex === null) return;
488
+
489
  const quantityInput = document.getElementById('quantityInput');
490
  const quantity = parseInt(quantityInput.value);
491
  const colorSelect = document.getElementById('colorSelect');
492
  const color = colorSelect.style.display !== 'none' && colorSelect.value ? colorSelect.value : 'N/A';
493
+
494
  if (isNaN(quantity) || quantity <= 0) {
495
  alert("Пожалуйста, укажите корректное количество (больше 0).");
496
  quantityInput.focus();
497
  return;
498
  }
499
+
500
  const product = products[selectedProductIndex];
501
  if (!product) {
502
  alert("Ошибка добавления: товар не найден.");
503
  return;
504
  }
505
+
506
  const cartItemId = `${product.name}-${color}`;
507
  const existingItemIndex = cart.findIndex(item => item.id === cartItemId);
508
+
509
  if (existingItemIndex > -1) {
510
  cart[existingItemIndex].quantity += quantity;
511
  } else {
 
518
  color: color
519
  });
520
  }
521
+
522
+ localStorage.setItem('mekaCart', JSON.stringify(cart));
523
  closeModal('quantityModal');
524
  updateCartButton();
525
  showNotification(`${product.name} добавлен в корзину!`);
526
  }
527
+
528
  function updateCartButton() {
529
  const cartCountElement = document.getElementById('cart-count');
530
  const cartButton = document.getElementById('cart-button');
531
  if (!cartCountElement || !cartButton) return;
532
+
533
  let totalItems = 0;
534
  cart.forEach(item => { totalItems += item.quantity; });
535
+
536
  if (totalItems > 0) {
537
  cartCountElement.textContent = totalItems;
538
  cartButton.style.display = 'flex';
 
541
  cartButton.style.display = 'none';
542
  }
543
  }
544
+
545
  function openCartModal() {
546
  const cartContent = document.getElementById('cartContent');
547
  const cartTotalElement = document.getElementById('cartTotal');
548
  if (!cartContent || !cartTotalElement) return;
549
+
550
  let total = 0;
551
+
552
  if (cart.length === 0) {
553
  cartContent.innerHTML = '<p style="text-align: center; padding: 20px;">Ваша корзина пуста.</p>';
554
  cartTotalElement.textContent = '0.00';
 
560
  ? `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${item.photo}`
561
  : 'https://via.placeholder.com/60x60.png?text=N/A';
562
  const colorText = item.color !== 'N/A' ? ` (Цвет: ${item.color})` : '';
563
+
564
  return `
565
  <div class="cart-item">
566
  <img src="${photoUrl}" alt="${item.name}">
 
581
  document.body.style.overflow = 'hidden';
582
  }
583
  }
584
+
585
  function removeFromCart(itemId) {
586
  cart = cart.filter(item => item.id !== itemId);
587
+ localStorage.setItem('mekaCart', JSON.stringify(cart));
588
  openCartModal();
589
  updateCartButton();
590
  }
591
+
592
  function clearCart() {
593
  if (confirm("Вы уверены, что хотите очистить корзину?")) {
594
  cart = [];
595
+ localStorage.removeItem('mekaCart');
596
  openCartModal();
597
  updateCartButton();
598
  }
599
  }
600
+
601
  function formulateOrder() {
602
  if (cart.length === 0) {
603
  alert("Корзина пуста! Добавьте товары перед формированием заказа.");
604
  return;
605
  }
606
+
607
  const orderData = {
608
  cart: cart
609
  };
610
+
611
  const formulateButton = document.querySelector('.formulate-order-button');
612
  if (formulateButton) formulateButton.disabled = true;
613
+
614
  showNotification("Формируем заказ...", 5000);
615
+
616
  fetch('/create_order', {
617
  method: 'POST',
618
  headers: { 'Content-Type': 'application/json' },
 
626
  })
627
  .then(data => {
628
  if (data.order_id) {
629
+ localStorage.removeItem('mekaCart');
630
  cart = [];
631
  updateCartButton();
632
  closeModal('cartModal');
 
641
  if (formulateButton) formulateButton.disabled = false;
642
  });
643
  }
644
+
645
+
646
  function filterProducts() {
647
  const searchTerm = document.getElementById('search-input').value.toLowerCase().trim();
648
  const activeCategoryButton = document.querySelector('.category-filter.active');
649
  const activeCategory = activeCategoryButton ? activeCategoryButton.dataset.category : 'all';
650
  const grid = document.getElementById('products-grid');
651
  let visibleProducts = 0;
652
+
653
  const existingNoResults = grid.querySelector('.no-results-message');
654
  if (existingNoResults) existingNoResults.remove();
655
+
656
  document.querySelectorAll('.products-grid .product').forEach(productElement => {
657
  const name = productElement.getAttribute('data-name');
658
  const description = productElement.getAttribute('data-description');
659
  const category = productElement.getAttribute('data-category');
660
+
661
  const matchesSearch = !searchTerm || name.includes(searchTerm) || description.includes(searchTerm);
662
  const matchesCategory = activeCategory === 'all' || category === activeCategory;
663
+
664
  if (matchesSearch && matchesCategory) {
665
  productElement.style.display = 'flex';
666
  visibleProducts++;
 
668
  productElement.style.display = 'none';
669
  }
670
  });
671
+
672
  if (visibleProducts === 0 && products.length > 0) {
673
  const p = document.createElement('p');
674
  p.className = 'no-results-message';
 
681
  grid.appendChild(p);
682
  }
683
  }
684
+
685
  function setupFilters() {
686
  const searchInput = document.getElementById('search-input');
687
  const categoryFilters = document.querySelectorAll('.category-filter');
688
+
689
  if(searchInput) searchInput.addEventListener('input', filterProducts);
690
+
691
  categoryFilters.forEach(filter => {
692
  filter.addEventListener('click', function() {
693
  categoryFilters.forEach(f => f.classList.remove('active'));
 
697
  });
698
  filterProducts();
699
  }
700
+
701
  function showNotification(message, duration = 3000) {
702
  const placeholder = document.getElementById('notification-placeholder');
703
  if (!placeholder) {
 
711
  document.body.appendChild(newPlaceholder);
712
  placeholder = newPlaceholder;
713
  }
714
+
715
+
716
  const notification = document.createElement('div');
717
  notification.className = 'notification';
718
  notification.textContent = message;
719
  placeholder.appendChild(notification);
720
+
721
  void notification.offsetWidth;
722
+
723
  notification.classList.add('show');
724
+
725
  setTimeout(() => {
726
  notification.classList.remove('show');
727
  notification.addEventListener('transitionend', () => notification.remove());
728
  }, duration);
729
  }
730
+
731
  document.addEventListener('DOMContentLoaded', () => {
 
732
  updateCartButton();
733
  setupFilters();
734
+
735
  window.addEventListener('click', function(event) {
736
  if (event.target.classList.contains('modal')) {
737
  closeModal(event.target.id);
738
  }
739
  });
740
+
741
  window.addEventListener('keydown', function(event) {
742
  if (event.key === 'Escape') {
743
  document.querySelectorAll('.modal[style*="display: block"]').forEach(modal => {
 
746
  }
747
  });
748
  });
749
+
750
  </script>
751
  </body>
752
  </html>
753
  '''
754
+
755
  PRODUCT_DETAIL_TEMPLATE = '''
756
  <div style="padding: 10px;">
757
+ <h2 style="font-size: 1.6rem; font-weight: 600; margin-bottom: 15px; text-align: center; color: #E91E63;">{{ product['name'] }}</h2>
758
+ <div class="swiper-container" style="max-width: 450px; margin: 0 auto 20px; border-radius: 10px; overflow: hidden; background-color: #fff; border: 1px solid #e0e0e0;">
759
  <div class="swiper-wrapper">
760
  {% if product.get('photos') and product['photos']|length > 0 %}
761
  {% for photo in product['photos'] %}
 
775
  </div>
776
  {% if product.get('photos') and product['photos']|length > 1 %}
777
  <div class="swiper-pagination" style="position: relative; bottom: 5px;"></div>
778
+ <div class="swiper-button-next" style="color: #E91E63;"></div>
779
+ <div class="swiper-button-prev" style="color: #E91E63;"></div>
780
  {% endif %}
781
  </div>
782
+
783
+ <div style="margin-top: 20px; font-size: 1rem; line-height: 1.7; color: #333;">
784
  <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
785
+ <p style="font-size: 1.2rem; font-weight: bold; color: #C2185B;"><strong>Цена:</strong> {{ "%.2f"|format(product['price']) }} {{ currency_code }}</p>
786
  <p><strong>Описание:</strong><br> {{ product.get('description', 'Описание отсутствует.')|replace('\\n', '<br>')|safe }}</p>
787
  {% set colors = product.get('colors', []) %}
788
  {% if colors and colors|select('ne', '')|list|length > 0 %}
 
791
  </div>
792
  </div>
793
  '''
794
+
795
  ORDER_TEMPLATE = '''
796
  <!DOCTYPE html>
797
  <html lang="ru">
 
802
  <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
803
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
804
  <style>
805
+ body { font-family: 'Poppins', sans-serif; background: #ffffff; color: #333; line-height: 1.6; padding: 20px; }
806
+ .container { max-width: 800px; margin: 20px auto; padding: 30px; background: #fff; border-radius: 15px; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08); border: 1px solid #e0e0e0; }
807
+ h1 { text-align: center; color: #E91E63; margin-bottom: 25px; font-size: 1.8rem; font-weight: 600; }
808
+ h2 { color: #C2185B; margin-top: 30px; margin-bottom: 15px; font-size: 1.4rem; border-bottom: 1px solid #e0e0e0; padding-bottom: 8px;}
809
+ .order-meta { font-size: 0.9rem; color: #999; margin-bottom: 20px; text-align: center; }
810
+ .order-item { display: grid; grid-template-columns: 60px 1fr auto; gap: 15px; align-items: center; padding: 15px 0; border-bottom: 1px solid #f0f0f0; }
811
  .order-item:last-child { border-bottom: none; }
812
+ .order-item img { width: 60px; height: 60px; object-fit: contain; border-radius: 8px; background-color: #fff; padding: 5px; border: 1px solid #e0e0e0;}
813
+ .item-details strong { display: block; margin-bottom: 5px; font-size: 1.05rem; color: #333;}
814
+ .item-details span { font-size: 0.9rem; color: #666; display: block;}
815
+ .item-total { font-weight: bold; text-align: right; font-size: 1rem; color: #C2185B;}
816
+ .order-summary { margin-top: 30px; padding-top: 20px; border-top: 2px solid #E91E63; text-align: right; }
817
  .order-summary p { margin-bottom: 10px; font-size: 1.1rem; }
818
+ .order-summary strong { font-size: 1.3rem; color: #E91E63; }
819
+ .customer-info { margin-top: 30px; background-color: #f9f9f9; padding: 20px; border-radius: 8px; border: 1px solid #e0e0e0;}
820
  .customer-info p { margin-bottom: 8px; font-size: 0.95rem; }
821
+ .customer-info strong { color: #C2185B; }
822
  .actions { margin-top: 30px; text-align: center; }
823
+ .button { padding: 12px 25px; border: none; border-radius: 8px; background-color: #E91E63; color: white; font-weight: 600; cursor: pointer; transition: background-color 0.3s ease, transform 0.1s ease; font-size: 1rem; display: inline-flex; align-items: center; gap: 8px; text-decoration: none; }
824
+ .button:hover { background-color: #C2185B; }
825
  .button:active { transform: scale(0.98); }
826
  .button i { font-size: 1.2rem; }
827
+ .catalog-link { display: block; text-align: center; margin-top: 25px; color: #E91E63; text-decoration: none; font-size: 0.9rem; }
828
  .catalog-link:hover { text-decoration: underline; }
829
+ .not-found { text-align: center; color: #dc3545; font-size: 1.2rem; padding: 40px 0;}
830
  </style>
831
  </head>
832
  <body>
 
834
  {% if order %}
835
  <h1><i class="fas fa-receipt"></i> Ваш Заказ №{{ order.id }}</h1>
836
  <p class="order-meta">Дата создания: {{ order.created_at }}</p>
837
+
838
  <h2><i class="fas fa-shopping-bag"></i> Товары в заказе</h2>
839
  <div id="orderItems">
840
  {% for item in order.cart %}
 
850
  </div>
851
  {% endfor %}
852
  </div>
853
+
854
  <div class="order-summary">
855
  <p>Общая сумма товаров: <strong>{{ "%.2f"|format(order.total_price) }} {{ currency_code }}</strong></p>
856
  <p><strong>ИТОГО К ОПЛАТЕ: {{ "%.2f"|format(order.total_price) }} {{ currency_code }}</strong></p>
857
  </div>
858
+
859
  <div class="customer-info">
860
  <h2><i class="fas fa-info-circle"></i> Статус заказа</h2>
861
  <p>Этот заказ был оформлен без входа в систему.</p>
862
  <p>Пожалуйста, свяжитесь с нами по WhatsApp для подтверждения и уточнения деталей.</p>
863
  </div>
864
+
865
  <div class="actions">
866
  <button class="button" onclick="sendOrderViaWhatsApp()"><i class="fab fa-whatsapp"></i> Отправить заказ</button>
867
  </div>
868
+
869
  <a href="{{ url_for('catalog') }}" class="catalog-link">← Вернуться в каталог</a>
870
+
871
  <script>
872
  function sendOrderViaWhatsApp() {
873
  const orderId = '{{ order.id }}';
874
  const orderUrl = `{{ request.url }}`;
875
+ const whatsappNumber = "996509455959";
876
+
877
  let message = `Здравствуйте! Хочу подтвердить свой заказ на Meka Shop:%0A%0A`;
878
  message += `*Номер заказа:* ${orderId}%0A`;
879
  message += `*Ссылка на заказ:* ${encodeURIComponent(orderUrl)}%0A%0A`;
880
  message += `Пожалуйста, свяжитесь со мной для уточнения деталей оплаты и доставки.`;
881
+
882
  const whatsappUrl = `https://api.whatsapp.com/send?phone=${whatsappNumber}&text=${message}`;
883
  window.open(whatsappUrl, '_blank');
884
  }
885
  </script>
886
+
887
  {% else %}
888
+ <h1 style="color: #dc3545;"><i class="fas fa-exclamation-triangle"></i> Ошибка</h1>
889
  <p class="not-found">Заказ с таким ID не найден.</p>
890
  <a href="{{ url_for('catalog') }}" class="catalog-link">← Вернуться в каталог</a>
891
  {% endif %}
 
893
  </body>
894
  </html>
895
  '''
896
+
897
  ADMIN_TEMPLATE = '''
898
  <!DOCTYPE html>
899
  <html lang="ru">
 
904
  <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
905
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
906
  <style>
907
+ body { font-family: 'Poppins', sans-serif; background-color: #ffffff; color: #333; padding: 20px; line-height: 1.6; }
908
  .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); }
909
+ .header { padding-bottom: 15px; margin-bottom: 25px; border-bottom: 1px solid #e0e0e0; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px;}
910
+ h1, h2, h3 { font-weight: 600; color: #E91E63; margin-bottom: 15px; }
911
  h1 { font-size: 1.8rem; }
912
  h2 { font-size: 1.5rem; margin-top: 30px; display: flex; align-items: center; gap: 8px; }
913
+ h3 { font-size: 1.2rem; color: #C2185B; margin-top: 20px; }
914
+ .section { margin-bottom: 30px; padding: 20px; background-color: #f9f9f9; border: 1px solid #e0e0e0; border-radius: 8px; }
915
  form { margin-bottom: 20px; }
916
+ label { font-weight: 500; margin-top: 10px; display: block; color: #666; font-size: 0.9rem;}
917
+ input[type="text"], input[type="number"], input[type="password"], input[type="tel"], textarea, select { width: 100%; padding: 10px 12px; margin-top: 5px; border: 1px solid #e0e0e0; border-radius: 6px; font-size: 0.95rem; box-sizing: border-box; transition: border-color 0.3s ease; background-color: #fff; }
918
+ input:focus, textarea:focus, select:focus { border-color: #E91E63; outline: none; box-shadow: 0 0 0 2px rgba(233, 30, 99, 0.1); }
919
  textarea { min-height: 80px; resize: vertical; }
920
+ input[type="file"] { padding: 8px; background-color: #ffffff; cursor: pointer; border: 1px solid #e0e0e0;}
921
+ input[type="file"]::file-selector-button { padding: 5px 10px; border-radius: 4px; background-color: #f0f0f0; border: 1px solid #e0e0e0; cursor: pointer; margin-right: 10px;}
922
  input[type="checkbox"] { margin-right: 5px; vertical-align: middle; }
923
  label.inline-label { display: inline-block; margin-top: 10px; font-weight: normal; }
924
+ button, .button { padding: 10px 18px; border: none; border-radius: 6px; background-color: #F06292; 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;}
925
+ button:hover, .button:hover { background-color: #E91E63; }
926
  button:active, .button:active { transform: scale(0.98); }
927
  button[type="submit"] { min-width: 120px; justify-content: center; }
928
+ .delete-button { background-color: #dc3545; }
929
+ .delete-button:hover { background-color: #c82333; }
930
+ .add-button { background-color: #E91E63; }
931
+ .add-button:hover { background-color: #C2185B; }
932
  .item-list { display: grid; gap: 20px; }
933
+ .item { background: #fff; padding: 15px 20px; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.03); border: 1px solid #f0f0f0; }
934
+ .item p { margin: 5px 0; font-size: 0.9rem; color: #666; }
935
+ .item strong { color: #333; }
936
+ .item .description { font-size: 0.85rem; color: #999; max-height: 60px; overflow: hidden; text-overflow: ellipsis; }
937
  .item-actions { margin-top: 15px; display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
938
+ .item-actions button:not(.delete-button) { background-color: #F06292; }
939
+ .item-actions button:not(.delete-button):hover { background-color: #E91E63; }
940
+ .edit-form-container { margin-top: 15px; padding: 20px; background: #fff7fa; border: 1px dashed #e0e0e0; border-radius: 6px; display: none; }
941
+ details { background-color: #f9f9f9; border: 1px solid #e0e0e0; border-radius: 8px; margin-bottom: 20px; }
942
+ details > summary { cursor: pointer; font-weight: 600; color: #C2185B; display: block; padding: 15px; border-bottom: 1px solid #e0e0e0; list-style: none; position: relative; }
943
+ details > summary::after { content: '\\f078'; font-family: 'Font Awesome 6 Free'; font-weight: 900; position: absolute; right: 20px; top: 50%; transform: translateY(-50%); transition: transform 0.2s ease; color: #E91E63; }
944
  details[open] > summary::after { transform: translateY(-50%) rotate(180deg); }
945
+ details[open] > summary { border-bottom: 1px solid #e0e0e0; }
946
  details .form-content { padding: 20px; }
947
  .color-input-group { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
948
  .color-input-group input { flex-grow: 1; margin: 0; }
949
+ .remove-color-btn { background-color: #dc3545; padding: 6px 10px; font-size: 0.8rem; margin-top: 0; line-height: 1; }
950
+ .remove-color-btn:hover { background-color: #c82333; }
951
+ .add-color-btn { background-color: #F8BBD0; color: #C2185B; border: 1px solid #e0e0e0; }
952
+ .add-color-btn:hover { background-color: #E91E63; color: white; border-color: #E91E63; }
953
+ .photo-preview img { max-width: 70px; max-height: 70px; border-radius: 5px; margin: 5px 5px 0 0; border: 1px solid #e0e0e0; object-fit: cover;}
954
  .sync-buttons { display: flex; gap: 10px; margin-bottom: 20px; flex-wrap: wrap; }
955
+ .download-hf-button { background-color: #6c757d; }
956
+ .download-hf-button:hover { background-color: #5a6268; }
957
  .flex-container { display: flex; flex-wrap: wrap; gap: 20px; }
958
  .flex-item { flex: 1; min-width: 350px; }
959
  .message { padding: 10px 15px; border-radius: 6px; margin-bottom: 15px; font-size: 0.9rem;}
960
+ .message.success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb;}
961
+ .message.error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb;}
962
+ .message.warning { background-color: #fff3cd; color: #856404; border: 1px solid #ffeeba; }
963
  .status-indicator { display: inline-block; padding: 3px 8px; border-radius: 12px; font-size: 0.8rem; font-weight: 500; margin-left: 10px; vertical-align: middle; }
964
+ .status-indicator.in-stock { background-color: #d4edda; color: #155724; }
965
+ .status-indicator.out-of-stock { background-color: #f8d7da; color: #721c24; }
966
+ .status-indicator.top-product { background-color: #fff3cd; color: #856404; margin-left: 5px;}
967
  </style>
968
  </head>
969
  <body>
970
  <div class="container">
971
  <div class="header">
972
  <div class="logo-title-container" style="display: flex; align-items: center; gap: 15px;">
 
973
  <h1><i class="fas fa-tools"></i> Админ-панель Meka Shop</h1>
974
  </div>
975
+ <a href="{{ url_for('catalog') }}" class="button" style="background-color: #E91E63;"><i class="fas fa-store"></i> Перейти в каталог</a>
976
  </div>
977
+
978
  {% with messages = get_flashed_messages(with_categories=true) %}
979
  {% if messages %}
980
  {% for category, message in messages %}
 
982
  {% endfor %}
983
  {% endif %}
984
  {% endwith %}
985
+
986
  <div class="section">
987
  <h2><i class="fas fa-sync-alt"></i> Синхронизация с Датацентром</h2>
988
  <div class="sync-buttons">
 
993
  <button type="submit" class="button download-hf-button" title="Скачать файлы (перезапишет локальные)"><i class="fas fa-download"></i> Скачать БД</button>
994
  </form>
995
  </div>
996
+ <p style="font-size: 0.85rem; color: #999;">Резервное копирование происходит автоматически каждые 30 минут, а также после каждого сохранения данных. Используйте эти кнопки для немедленной синхронизации.</p>
997
  </div>
998
+
999
  <div class="flex-container">
1000
  <div class="flex-item">
1001
  <div class="section">
 
1011
  </form>
1012
  </div>
1013
  </details>
1014
+
1015
  <h3>Существующие категории:</h3>
1016
  {% if categories %}
1017
  <div class="item-list">
 
1031
  {% endif %}
1032
  </div>
1033
  </div>
1034
+
1035
  <div class="flex-item">
1036
  <div class="section">
1037
  <h2><i class="fas fa-info-circle"></i> Информация</h2>
 
1040
  </div>
1041
  </div>
1042
  </div>
1043
+
1044
  <div class="section">
1045
  <h2><i class="fas fa-box-open"></i> Управление товарами</h2>
1046
  <details>
 
1085
  </form>
1086
  </div>
1087
  </details>
1088
+
1089
  <h3>Список товаров:</h3>
1090
  {% if products %}
1091
  <div class="item-list">
 
1102
  {% endif %}
1103
  </div>
1104
  <div style="flex-grow: 1;">
1105
+ <h3 style="margin-top: 0; margin-bottom: 5px; color: #333;">
1106
  {{ product['name'] }}
1107
  {% if product.get('in_stock', True) %}
1108
  <span class="status-indicator in-stock">В наличии</span>
 
1119
  {% set colors = product.get('colors', []) %}
1120
  <p><strong>Цвета/Вар-ты:</strong> {{ colors|select('ne', '')|join(', ') if colors|select('ne', '')|list|length > 0 else 'Нет' }}</p>
1121
  {% if product.get('photos') and product['photos']|length > 1 %}
1122
+ <p style="font-size: 0.8rem; color: #999;">(Всего фото: {{ product['photos']|length }})</p>
1123
  {% endif %}
1124
  </div>
1125
  </div>
1126
+
1127
  <div class="item-actions">
1128
  <button type="button" class="button" onclick="toggleEditForm('edit-form-{{ loop.index0 }}')"><i class="fas fa-edit"></i> Редактировать</button>
1129
  <form method="POST" style="margin:0;" onsubmit="return confirm('Вы уверены, что хотите удалить товар \'{{ product['name'] }}\'?');">
 
1132
  <button type="submit" class="delete-button"><i class="fas fa-trash-alt"></i> Удалить</button>
1133
  </form>
1134
  </div>
1135
+
1136
  <div id="edit-form-{{ loop.index0 }}" class="edit-form-container">
1137
  <h4><i class="fas fa-edit"></i> Редактирование: {{ product['name'] }}</h4>
1138
  <form method="POST" enctype="multipart/form-data">
 
1201
  <p>Товаров пока нет.</p>
1202
  {% endif %}
1203
  </div>
1204
+
1205
  </div>
1206
+
1207
  <script>
1208
  function toggleEditForm(formId) {
1209
  const formContainer = document.getElementById(formId);
 
1211
  formContainer.style.display = formContainer.style.display === 'none' || formContainer.style.display === '' ? 'block' : 'none';
1212
  }
1213
  }
1214
+
1215
  function addColorInput(containerId) {
1216
  const container = document.getElementById(containerId);
1217
  if (container) {
 
1228
  }
1229
  }
1230
  }
1231
+
1232
  function removeColorInput(button) {
1233
  const group = button.closest('.color-input-group');
1234
  if (group) {
 
1251
  </body>
1252
  </html>
1253
  '''
1254
+
1255
+
1256
  @app.route('/')
1257
  def catalog():
1258
  data = load_data()
1259
  all_products = data.get('products', [])
1260
  categories = sorted(data.get('categories', []))
1261
+
1262
  products_in_stock = [p for p in all_products if p.get('in_stock', True)]
1263
  products_sorted = sorted(products_in_stock, key=lambda p: (not p.get('is_top', False), p.get('name', '').lower()))
1264
+
1265
  return render_template_string(
1266
  CATALOG_TEMPLATE,
1267
  products=products_sorted,
 
1270
  store_address=STORE_ADDRESS,
1271
  currency_code=CURRENCY_CODE
1272
  )
1273
+
1274
  @app.route('/product/<int:index>')
1275
  def product_detail(index):
1276
  data = load_data()
1277
  all_products = data.get('products', [])
1278
  products_in_stock = [p for p in all_products if p.get('in_stock', True)]
1279
  products_sorted = sorted(products_in_stock, key=lambda p: (not p.get('is_top', False), p.get('name', '').lower()))
1280
+
1281
  try:
1282
  product = products_sorted[index]
1283
  except IndexError:
1284
  logging.warning(f"Attempted access to non-existent or out-of-stock product with index {index}")
1285
  return "Товар не найден или отсутствует в наличии.", 404
1286
+
1287
  return render_template_string(
1288
  PRODUCT_DETAIL_TEMPLATE,
1289
  product=product,
1290
  repo_id=REPO_ID,
1291
  currency_code=CURRENCY_CODE
1292
  )
1293
+
1294
  @app.route('/create_order', methods=['POST'])
1295
  def create_order():
1296
  order_data = request.get_json()
1297
+
1298
  if not order_data or 'cart' not in order_data or not order_data['cart']:
1299
  logging.warning("Create order request missing cart data or cart is empty.")
1300
  return jsonify({"error": "Корзина пуста или не передана."}), 400
1301
+
1302
  cart_items = order_data['cart']
1303
+
1304
  total_price = 0
1305
  processed_cart = []
1306
  for item in cart_items:
 
1324
  except (ValueError, TypeError) as e:
1325
  logging.error(f"Invalid price/quantity in cart item: {item}. Error: {e}")
1326
  return jsonify({"error": "Неверная цена или количество в товаре."}), 400
1327
+
1328
  order_id = f"{datetime.now().strftime('%Y%m%d%H%M%S')}-{uuid.uuid4().hex[:6]}"
1329
  order_timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
1330
+
1331
  new_order = {
1332
  "id": order_id,
1333
  "created_at": order_timestamp,
 
1336
  "user_info": None,
1337
  "status": "new"
1338
  }
1339
+
1340
  try:
1341
  data = load_data()
1342
  if 'orders' not in data or not isinstance(data.get('orders'), dict):
1343
  data['orders'] = {}
1344
+
1345
  data['orders'][order_id] = new_order
1346
  save_data(data)
1347
  logging.info(f"Order {order_id} created successfully (anonymously).")
1348
  return jsonify({"order_id": order_id}), 201
1349
+
1350
  except Exception as e:
1351
  logging.error(f"Failed to save order {order_id}: {e}", exc_info=True)
1352
  return jsonify({"error": "Ошибка серве��а при сохранении заказа."}), 500
1353
+
1354
+
1355
  @app.route('/order/<order_id>')
1356
  def view_order(order_id):
1357
  data = load_data()
1358
  order = data.get('orders', {}).get(order_id)
1359
+
1360
  if order:
1361
  logging.info(f"Displaying order {order_id}")
1362
  else:
1363
  logging.warning(f"Order {order_id} not found.")
1364
+
1365
  return render_template_string(ORDER_TEMPLATE,
1366
  order=order,
1367
  repo_id=REPO_ID,
1368
+ currency_code=CURRENCY_CODE)
1369
+
1370
+
1371
  @app.route('/admin', methods=['GET', 'POST'])
1372
  def admin():
1373
  data = load_data()
 
1375
  categories = data.get('categories', [])
1376
  if 'orders' not in data or not isinstance(data.get('orders'), dict):
1377
  data['orders'] = {}
1378
+
1379
  if request.method == 'POST':
1380
  action = request.form.get('action')
1381
  logging.info(f"Admin action received: {action}")
1382
+
1383
  try:
1384
  if action == 'add_category':
1385
  category_name = request.form.get('category_name', '').strip()
 
1395
  else:
1396
  logging.warning(f"Category '{category_name}' already exists.")
1397
  flash(f"Категория '{category_name}' уже существует.", 'error')
1398
+
1399
  elif action == 'delete_category':
1400
  category_to_delete = request.form.get('category_name')
1401
  if category_to_delete and category_to_delete in categories:
 
1413
  else:
1414
  logging.warning(f"Attempted to delete non-existent or empty category: {category_to_delete}")
1415
  flash(f"Не удалось удалить категорию '{category_to_delete}'.", 'error')
1416
+
1417
  elif action == 'add_product':
1418
  name = request.form.get('name', '').strip()
1419
  price_str = request.form.get('price', '').replace(',', '.')
 
1423
  colors = [c.strip() for c in request.form.getlist('colors') if c.strip()]
1424
  in_stock = 'in_stock' in request.form
1425
  is_top = 'is_top' in request.form
1426
+
1427
  if not name or not price_str:
1428
  flash("Название и цена товара обязательны.", 'error')
1429
  return redirect(url_for('admin'))
1430
+
1431
  try:
1432
  price = round(float(price_str), 2)
1433
  if price < 0: price = 0
1434
  except ValueError:
1435
  flash("Неверный формат цены.", 'error')
1436
  return redirect(url_for('admin'))
1437
+
1438
  photos_list = []
1439
  if photos_files and HF_TOKEN_WRITE:
1440
  uploads_dir = 'uploads_temp'
 
1454
  logging.warning(f"Skipping non-image file upload: {photo.filename}")
1455
  flash(f"Файл {photo.filename} не является изображением и был пропущен.", "warning")
1456
  continue
1457
+
1458
  safe_name = secure_filename(name.replace(' ', '_'))[:50]
1459
  photo_filename = f"{safe_name}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{ext}"
1460
  temp_path = os.path.join(uploads_dir, photo_filename)
 
1487
  logging.warning(f"Could not remove temporary upload directory {uploads_dir}: {e}")
1488
  elif not HF_TOKEN_WRITE and photos_files and any(f.filename for f in photos_files):
1489
  flash("HF_TOKEN (write) не настроен. Фотографии не были загружены.", "warning")
1490
+
1491
+
1492
  new_product = {
1493
  'name': name, 'price': price, 'description': description,
1494
  'category': category if category in categories else 'Без категории',
 
1500
  save_data(data)
1501
  logging.info(f"Product '{name}' added.")
1502
  flash(f"Товар '{name}' успешно добавлен.", 'success')
1503
+
1504
  elif action == 'edit_product':
1505
  index_str = request.form.get('index')
1506
  if index_str is None:
1507
  flash("Ошибка редактирования: индекс товара не передан.", 'error')
1508
  return redirect(url_for('admin'))
1509
+
1510
  try:
1511
  index = int(index_str)
1512
  if not (0 <= index < len(products)):
1513
  raise IndexError("Product index out of range")
1514
  product_to_edit = products[index]
1515
  original_name = product_to_edit.get('name', 'N/A')
1516
+
1517
  except (ValueError, IndexError):
1518
  flash(f"Ошибка редактирования: неверный индекс товара '{index_str}'.", 'error')
1519
  logging.error(f"Invalid index '{index_str}' for editing. Product list length: {len(products)}")
1520
  return redirect(url_for('admin'))
1521
+
1522
  product_to_edit['name'] = request.form.get('name', product_to_edit['name']).strip()
1523
  price_str = request.form.get('price', str(product_to_edit['price'])).replace(',', '.')
1524
  product_to_edit['description'] = request.form.get('description', product_to_edit.get('description', '')).strip()
 
1527
  product_to_edit['colors'] = [c.strip() for c in request.form.getlist('colors') if c.strip()]
1528
  product_to_edit['in_stock'] = 'in_stock' in request.form
1529
  product_to_edit['is_top'] = 'is_top' in request.form
1530
+
1531
  try:
1532
  price = round(float(price_str), 2)
1533
  if price < 0: price = 0
 
1535
  except ValueError:
1536
  logging.warning(f"Invalid price format '{price_str}' during edit of product {original_name}. Price not changed.")
1537
  flash(f"Неверный формат цены для товара '{original_name}'. Цена не изменена.", 'warning')
1538
+
1539
  photos_files = request.files.getlist('photos')
1540
  if photos_files and any(f.filename for f in photos_files) and HF_TOKEN_WRITE:
1541
  uploads_dir = 'uploads_temp'
 
1557
  logging.warning(f"Skipping non-image file upload during edit: {photo.filename}")
1558
  flash(f"Файл {photo.filename} не является изображением и был пропущен.", "warning")
1559
  continue
1560
+
1561
  safe_name = secure_filename(product_to_edit['name'].replace(' ', '_'))[:50]
1562
  photo_filename = f"{safe_name}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{ext}"
1563
  temp_path = os.path.join(uploads_dir, photo_filename)
 
1581
  os.rmdir(uploads_dir)
1582
  except OSError as e:
1583
  logging.warning(f"Could not remove temporary upload directory {uploads_dir}: {e}")
1584
+
1585
  if new_photos_list:
1586
  logging.info(f"New photo list for product {product_to_edit['name']} generated.")
1587
  old_photos = product_to_edit.get('photos', [])
 
1605
  flash("Не удалось загрузить новые фотографии (возможно, неверный формат).", "error")
1606
  elif not HF_TOKEN_WRITE and photos_files and any(f.filename for f in photos_files):
1607
  flash("HF_TOKEN (write) не настроен. Фотографии не были обновлены.", "warning")
1608
+
1609
  products[index] = product_to_edit
1610
  data['products'] = products
1611
  save_data(data)
1612
  logging.info(f"Product '{original_name}' (index {index}) updated to '{product_to_edit['name']}'.")
1613
  flash(f"Товар '{product_to_edit['name']}' успешно обновлен.", 'success')
1614
+
1615
  elif action == 'delete_product':
1616
  index_str = request.form.get('index')
1617
  if index_str is None:
 
1622
  if not (0 <= index < len(products)): raise IndexError("Product index out of range")
1623
  deleted_product = products.pop(index)
1624
  product_name = deleted_product.get('name', 'N/A')
1625
+
1626
  photos_to_delete = deleted_product.get('photos', [])
1627
  if photos_to_delete and HF_TOKEN_WRITE:
1628
  logging.info(f"Attempting to delete photos for product '{product_name}' from HF: {photos_to_delete}")
 
1642
  elif photos_to_delete and not HF_TOKEN_WRITE:
1643
  logging.warning(f"HF_TOKEN (write) not set. Cannot delete photos {photos_to_delete} for deleted product '{product_name}'.")
1644
  flash(f"Товар '{product_name}' удален локально, но фото не удалены с сервера (токен не задан).", "warning")
1645
+
1646
+
1647
  data['products'] = products
1648
  save_data(data)
1649
  logging.info(f"Product '{product_name}' (original index {index}) deleted.")
 
1651
  except (ValueError, IndexError):
1652
  flash(f"Ошибка удаления: неверный индекс товара '{index_str}'.", 'error')
1653
  logging.error(f"Invalid index '{index_str}' for deletion. Product list length: {len(products)}")
1654
+
1655
  else:
1656
  logging.warning(f"Received unknown admin action: {action}")
1657
  flash(f"Неизвестное действие: {action}", 'warning')
1658
+
1659
  return redirect(url_for('admin'))
1660
+
1661
  except Exception as e:
1662
+ logging.error(f"Произошла внутренняя ошибка при выполнении действия '{action}': {e}", exc_info=True)
1663
  flash(f"Произошла внутренняя ошибка при выполнении действия '{action}'. Подробности в логе сервера.", 'error')
1664
  return redirect(url_for('admin'))
1665
+
1666
  current_data = load_data()
1667
  display_products = sorted(current_data.get('products', []), key=lambda p: p.get('name', '').lower())
1668
  display_categories = sorted(current_data.get('categories', []))
1669
+
1670
  return render_template_string(
1671
  ADMIN_TEMPLATE,
1672
  products=display_products,
 
1674
  repo_id=REPO_ID,
1675
  currency_code=CURRENCY_CODE
1676
  )
1677
+
1678
  @app.route('/force_upload', methods=['POST'])
1679
  def force_upload():
1680
  logging.info("Forcing upload to Hugging Face...")
 
1685
  logging.error(f"Error during forced upload: {e}", exc_info=True)
1686
  flash(f"Ошибка при загрузке на Hugging Face: {e}", 'error')
1687
  return redirect(url_for('admin'))
1688
+
1689
  @app.route('/force_download', methods=['POST'])
1690
  def force_download():
1691
  logging.info("Forcing download from Hugging Face...")
 
1699
  logging.error(f"Error during forced download: {e}", exc_info=True)
1700
  flash(f"Ошибка при скачивании с Hugging Face: {e}", 'error')
1701
  return redirect(url_for('admin'))
1702
+
1703
+
1704
  if __name__ == '__main__':
1705
  logging.info("Application starting up. Performing initial data load/download...")
1706
  download_db_from_hf()
1707
  load_data()
1708
  logging.info("Initial data load complete.")
1709
+
1710
  if HF_TOKEN_WRITE:
1711
  backup_thread = threading.Thread(target=periodic_backup, daemon=True)
1712
  backup_thread.start()
1713
  logging.info("Periodic backup thread started.")
1714
  else:
1715
  logging.warning("Periodic backup will NOT run (HF_TOKEN for writing not set).")
1716
+
1717
  port = int(os.environ.get('PORT', 7860))
1718
  logging.info(f"Starting Flask app on host 0.0.0.0 and port {port}")
1719
  app.run(debug=False, host='0.0.0.0', port=port)