Kgshop commited on
Commit
2ac7066
·
verified ·
1 Parent(s): 8bc7947

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +277 -482
app.py CHANGED
@@ -15,7 +15,7 @@ import uuid
15
  load_dotenv()
16
 
17
  app = Flask(__name__)
18
- app.secret_key = 'raina_hvac_secret_key_98765_landing'
19
  DATA_FILE = 'data.json'
20
 
21
  SYNC_FILES = [DATA_FILE]
@@ -67,7 +67,7 @@ def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWN
67
  try:
68
  if file_name == DATA_FILE:
69
  with open(file_name, 'w', encoding='utf-8') as f:
70
- json.dump({'products': [], 'categories': [], 'services': []}, f)
71
  logging.info(f"Created empty local file {file_name} because it was not found on HF.")
72
  except Exception as create_e:
73
  logging.error(f"Failed to create empty local file {file_name}: {create_e}")
@@ -125,17 +125,17 @@ def periodic_backup():
125
  logging.info("Periodic backup finished.")
126
 
127
  def load_data():
128
- default_data = {'products': [], 'categories': [], 'services': []}
129
  try:
130
  with open(DATA_FILE, 'r', encoding='utf-8') as file:
131
  data = json.load(file)
132
  logging.info(f"Local data loaded successfully from {DATA_FILE}")
133
  if not isinstance(data, dict):
134
- logging.warning(f"Local {DATA_FILE} is not a dictionary. Attempting download.")
135
  raise FileNotFoundError
136
  if 'products' not in data: data['products'] = []
137
  if 'categories' not in data: data['categories'] = []
138
  if 'services' not in data: data['services'] = []
 
139
  return data
140
  except (FileNotFoundError, json.JSONDecodeError):
141
  logging.warning(f"Local file {DATA_FILE} not found or corrupt. Attempting download from HF.")
@@ -148,6 +148,7 @@ def load_data():
148
  if 'products' not in data: data['products'] = []
149
  if 'categories' not in data: data['categories'] = []
150
  if 'services' not in data: data['services'] = []
 
151
  return data
152
  except Exception as e:
153
  logging.error(f"Unknown error loading downloaded {DATA_FILE}: {e}. Using default.", exc_info=True)
@@ -171,6 +172,7 @@ def save_data(data):
171
  if 'products' not in data: data['products'] = []
172
  if 'categories' not in data: data['categories'] = []
173
  if 'services' not in data: data['services'] = []
 
174
  with open(DATA_FILE, 'w', encoding='utf-8') as file:
175
  json.dump(data, file, ensure_ascii=False, indent=4)
176
  logging.info(f"Data successfully saved to {DATA_FILE}")
@@ -206,7 +208,7 @@ LANDING_TEMPLATE = '''
206
  h1 { font-size: clamp(2.5rem, 6vw, 4rem); }
207
  h2 { font-size: clamp(2rem, 5vw, 3rem); text-align: center; margin-bottom: 60px; position: relative; }
208
  h2::after { content: ''; display: block; width: 80px; height: 4px; background: linear-gradient(90deg, var(--primary-color), var(--secondary-color)); margin: 15px auto 0; border-radius: 2px; }
209
- h3 { font-size: 1.5rem; color: var(--primary-color); margin-bottom: 15px; }
210
  p { margin-bottom: 1rem; color: var(--text-muted); }
211
  .btn { display: inline-block; padding: 12px 28px; background: linear-gradient(90deg, var(--primary-color), var(--secondary-color)); color: #fff; border-radius: 50px; text-decoration: none; font-weight: 600; transition: all 0.3s ease; box-shadow: 0 4px 15px var(--accent-glow); }
212
  .btn:hover { transform: translateY(-3px) scale(1.05); box-shadow: 0 8px 25px var(--accent-glow); }
@@ -215,45 +217,40 @@ LANDING_TEMPLATE = '''
215
  .header.scrolled { padding: 10px 0; box-shadow: 0 2px 10px rgba(0,0,0,0.3); }
216
  .navbar { display: flex; justify-content: space-between; align-items: center; }
217
  .logo { font-size: 1.8rem; font-weight: 700; color: #fff; text-decoration: none; }
218
- .nav-links { display: flex; gap: 30px; list-style: none; }
219
- .nav-links a { color: var(--text-color); text-decoration: none; font-weight: 600; transition: color 0.3s ease; }
220
- .nav-links a:hover { color: var(--primary-color); }
221
- .menu-toggle { display: none; font-size: 1.5rem; cursor: pointer; }
222
 
223
- #hero { height: 100vh; display: flex; align-items: center; background-image: linear-gradient(rgba(18, 18, 28, 0.7), rgba(18, 18, 28, 1)), url(https://i.imgur.com/k6O6XbJ.jpeg); background-size: cover; background-position: center; }
224
  .hero-content { text-align: center; max-width: 800px; margin: 0 auto; }
225
- .hero-content p { font-size: 1.2rem; margin: 30px 0; max-width: 600px; margin-left: auto; margin-right: auto;}
226
 
227
- .grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 60px; align-items: center; }
228
- .about-img { width: 100%; border-radius: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.4); }
229
 
230
- .services-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 30px; }
231
- .service-card { background-color: var(--card-bg); padding: 30px; border-radius: 15px; border: 1px solid #2a2a4a; transition: all 0.3s ease; }
232
- .service-card:hover { transform: translateY(-5px); border-color: var(--primary-color); box-shadow: 0 8px 25px var(--accent-glow); }
233
  .service-card i { font-size: 2.5rem; color: var(--primary-color); margin-bottom: 20px; }
234
-
235
- .turnkey-card { background-color: var(--card-bg); border-radius: 15px; overflow: hidden; border: 1px solid #2a2a4a; transition: all 0.3s ease; display: flex; flex-direction: column;}
236
- .turnkey-card:hover { transform: translateY(-5px); border-color: var(--primary-color); box-shadow: 0 8px 25px var(--accent-glow); }
237
- .turnkey-img { width: 100%; height: 200px; object-fit: cover; }
238
  .turnkey-content { padding: 30px; flex-grow: 1;}
239
 
240
- .equipment-filters { display: flex; justify-content: center; flex-wrap: wrap; gap: 15px; margin-bottom: 40px; }
241
- .filter-btn { padding: 8px 20px; border: 1px solid var(--primary-color); background-color: transparent; color: var(--primary-color); border-radius: 20px; cursor: pointer; transition: all 0.3s; }
242
  .filter-btn.active, .filter-btn:hover { background-color: var(--primary-color); color: #fff; }
243
- .equipment-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 30px; }
244
  .equipment-card { background-color: var(--card-bg); border-radius: 15px; overflow: hidden; text-align: center; padding: 20px; border: 1px solid #2a2a4a; transition: all 0.3s ease; }
245
  .equipment-card img { width: 100%; height: 180px; object-fit: contain; margin-bottom: 15px; }
246
  .equipment-card h3 { font-size: 1.2rem; }
247
  .equipment-card .price { font-size: 1.3rem; font-weight: 700; color: #fff; margin: 10px 0; }
248
 
249
- .projects-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 30px; }
250
- .project-card { position: relative; border-radius: 15px; overflow: hidden; }
251
  .project-card img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform 0.4s ease; }
252
  .project-overlay { position: absolute; bottom: 0; left: 0; right: 0; background: linear-gradient(to top, rgba(18,18,28,1) 0%, rgba(18,18,28,0) 100%); padding: 40px 20px 20px; transition: all 0.4s ease; }
253
- .project-card h3 { margin-bottom: 5px; font-size: 1.3rem; }
254
- .project-card p { margin-bottom: 0; transition: opacity 0.4s ease; opacity: 0; max-height: 0; overflow: hidden; }
255
  .project-card:hover img { transform: scale(1.05); }
256
- .project-card:hover .project-overlay { transform: translateY(0); }
257
  .project-card:hover p { opacity: 1; max-height: 200px; }
258
 
259
  #contact { background-color: var(--card-bg); }
@@ -261,38 +258,70 @@ LANDING_TEMPLATE = '''
261
  .contact-info { margin-top: 40px; display: flex; flex-direction: column; align-items: center; gap: 20px; }
262
  .contact-info p { font-size: 1.2rem; margin-bottom: 0; }
263
  .contact-info a { color: var(--primary-color); text-decoration: none; font-weight: 600; }
264
-
265
  .footer { text-align: center; padding: 30px 0; background-color: #0d0d14; }
266
 
267
- @media (max-width: 992px) {
268
- .grid-2 { grid-template-columns: 1fr; text-align: center; }
269
- .about-img { margin-bottom: 30px; max-width: 500px; margin-left: auto; margin-right: auto;}
 
270
  }
271
- @media (max-width: 768px) {
272
- .nav-links { position: fixed; top: 0; right: -100%; width: 250px; height: 100vh; background-color: var(--card-bg); flex-direction: column; justify-content: center; align-items: center; transition: right 0.4s ease-in-out; }
273
- .nav-links.active { right: 0; }
274
- .menu-toggle { display: block; }
275
- section { padding: 60px 0; }
276
- h2 { margin-bottom: 40px; }
277
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
278
  </style>
279
  </head>
280
  <body>
281
  <header class="header">
282
  <div class="container navbar">
283
  <a href="#" class="logo">Раина</a>
284
- <ul class="nav-links">
285
- <li><a href="#about">О компании</a></li>
286
- <li><a href="#services">Услуги</a></li>
287
- <li><a href="#turnkey">Под ключ</a></li>
288
- <li><a href="#equipment">Оборудование</a></li>
289
- <li><a href="#projects">Проекты</a></li>
290
- <li><a href="#contact">Контакты</a></li>
291
- </ul>
292
  <div class="menu-toggle"><i class="fas fa-bars"></i></div>
293
  </div>
294
  </header>
295
 
 
 
 
 
 
 
 
 
 
 
 
 
296
  <section id="hero">
297
  <div class="container hero-content">
298
  <h1>ОсОО "Раина": Ваш Партнер в Вентиляции и Кондиционировании</h1>
@@ -308,11 +337,11 @@ LANDING_TEMPLATE = '''
308
  <img src="https://i.imgur.com/8QhV42S.jpeg" alt="Команда Раина" class="about-img">
309
  <div>
310
  <h3>Основание и История</h3>
311
- <p>Компания "Раина" была основана в 2009 году. За эти годы мы зарекомендовали себя как надежный партнер, стремящийся к инновациям и совершенству в области к��иматических решений. Наш путь отмечен постоянным ростом и развитием.</p>
312
  <h3>Наша Миссия</h3>
313
- <p>Наша миссия — создание оптимального микроклимата для наших клиентов, обеспечивающего комфорт, здоровье и высокую производительность. Мы стремимся к тому, чтобы каждое помещение, оборудованное нашими системами, было источником чистого и свежего воздуха.</p>
314
  <h3>Профессиональная Команда</h3>
315
- <p>Наша команда состоит из высококвалифицированных и сертифицированных инженеров и техников, обладающих глубокими знаниями и опытом в области HVAC. Мы постоянно повышаем свою квалификацию, чтобы быть в курсе последних тенденций и технологий.</p>
316
  </div>
317
  </div>
318
  </div>
@@ -322,40 +351,22 @@ LANDING_TEMPLATE = '''
322
  <div class="container">
323
  <h2>Наши Услуги</h2>
324
  <div class="services-grid">
325
- <div class="service-card">
326
- <i class="fas fa-drafting-compass"></i>
327
- <h3>Проектирование</h3>
328
- <p>Мы выполняем точные расчеты, создаем детализированные 3D-модели и подготавливаем всю необходимую проектную документацию.</p>
329
- </div>
330
- <div class="service-card">
331
- <i class="fas fa-tools"></i>
332
- <h3>Монтаж</h3>
333
- <p>Осуществляем профессиональную установку всех типов систем HVAC, от бытовых кондиционеров до сложных промышленных систем.</p>
334
- </div>
335
- <div class="service-card">
336
- <i class="fas fa-headset"></i>
337
- <h3>Сервис и Обслуживание</h3>
338
- <p>Предлагаем полный спектр услуг по плановому техническому обслуживанию и аварийному ремонту. Служба поддержки работает 24/7.</p>
339
- </div>
340
- <div class="service-card">
341
- <i class="fas fa-sync-alt"></i>
342
- <h3>Модернизация</h3>
343
- <p>Помогаем повысить энергоэффективность существующих систем, внедряя современные решения и компоненты для снижения расходов.</p>
344
- </div>
345
  </div>
346
  </div>
347
  </section>
348
 
349
- <section id="turnkey" style="background-color: var(--dark-bg);">
350
  <div class="container">
351
  <h2>Услуги "под ключ"</h2>
352
  {% if services %}
353
  <div class="services-grid">
354
  {% for service in services %}
355
  <div class="turnkey-card">
356
- {% if service.photo %}
357
- <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/services/{{ service.photo }}" alt="{{ service.title }}" class="turnkey-img">
358
- {% endif %}
359
  <div class="turnkey-content">
360
  <h3><i class="{{ service.icon }} fa-fw" style="margin-right: 8px; color: var(--primary-color);"></i>{{ service.title }}</h3>
361
  <p>{{ service.description }}</p>
@@ -364,29 +375,23 @@ LANDING_TEMPLATE = '''
364
  {% endfor %}
365
  </div>
366
  {% else %}
367
- <p style="text-align: center;">Информация об услугах "под ключ" скоро появится на сайте.</p>
368
  {% endif %}
369
  </div>
370
  </section>
371
 
372
- <section id="equipment">
373
  <div class="container">
374
  <h2>Наше Оборудование</h2>
375
  {% if products %}
376
  <div class="equipment-filters">
377
  <button class="filter-btn active" data-filter="all">Все</button>
378
- {% for category in categories %}
379
- <button class="filter-btn" data-filter="{{ category }}">{{ category }}</button>
380
- {% endfor %}
381
  </div>
382
  <div class="equipment-grid">
383
  {% for product in products %}
384
  <div class="equipment-card" data-category="{{ product.get('category', 'all') }}">
385
- {% if product.photos %}
386
- <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product.photos[0] }}" alt="{{ product.name }}">
387
- {% else %}
388
- <img src="https://via.placeholder.com/250x180.png?text=No+Image" alt="No Image">
389
- {% endif %}
390
  <h3>{{ product.name }}</h3>
391
  <p class="price">{{ "%.2f"|format(product.price) }} KGS</p>
392
  <a href="https://api.whatsapp.com/send?phone={{ whatsapp_phone }}&text=Здравствуйте, интересует оборудование: {{ product.name }}" target="_blank" class="btn" style="padding: 8px 20px; font-size: 0.9rem;">Запросить</a>
@@ -398,56 +403,39 @@ LANDING_TEMPLATE = '''
398
  {% endif %}
399
  </div>
400
  </section>
401
-
402
  <section id="projects">
403
  <div class="container">
404
  <h2>Реализованные Проекты</h2>
 
405
  <div class="projects-grid">
 
406
  <div class="project-card">
407
- <img src="https://i.imgur.com/5V1fQ8u.jpeg" alt="Деловой Центр Заря">
408
  <div class="project-overlay">
409
- <h3>Деловой Центр "Заря"</h3>
410
- <p>Монтаж VRF-системы для 15 000 м² офисных площадей.</p>
411
- </div>
412
- </div>
413
- <div class="project-card">
414
- <img src="https://i.imgur.com/k6O6XbJ.jpeg" alt="Производственный Цех Техно">
415
- <div class="project-overlay">
416
- <h3>Производственный Цех "Техно"</h3>
417
- <p>Установка промышленной вентиляции для удаления вредных веществ.</p>
418
- </div>
419
- </div>
420
- <div class="project-card">
421
- <img src="https://i.imgur.com/z0JqY2h.jpeg" alt="Гостиница Аврора">
422
- <div class="project-overlay">
423
- <h3>Гостиница "Аврора"</h3>
424
- <p>Внедрение центральной системы кондиционирования для 120 номеров.</p>
425
- </div>
426
- </div>
427
- <div class="project-card">
428
- <img src="https://i.imgur.com/6U8kX7Z.jpeg" alt="Медицинский Центр Здоровье">
429
- <div class="project-overlay">
430
- <h3>Медицинский Центр "Здоровье"</h3>
431
- <p>Создание "чистых помещений" класса ISO 7 с высокоэффективной фильтрацией.</p>
432
  </div>
433
  </div>
 
434
  </div>
 
 
 
435
  </div>
436
  </section>
437
 
438
  <section id="contact">
439
  <div class="container contact-content">
440
  <h2>Контакты и Следующие Шаги</h2>
441
- <p>Благодарим вас за внимание к нашей презентации. Мы готовы стать вашим надежным партнером в создании идеального климата.</p>
442
  <div class="contact-info">
443
- <p><strong>Свяжитесь с нами по номеру:</strong> <a href="tel:{{ contact_phone }}">{{ contact_phone }}</a></p>
444
  <a href="https://api.whatsapp.com/send?phone={{ whatsapp_phone }}&text=Здравствуйте, я хотел(а) бы получить консультацию по вашим услугам." target="_blank" class="btn"><i class="fab fa-whatsapp"></i> Написать в WhatsApp</a>
445
  </div>
446
  <div style="margin-top: 40px; font-size: 0.9rem; color: var(--text-muted);">
447
- <p><strong>Наши реквизиты:</strong> ОсОО «Раина»</p>
448
- <p><strong>ИНН:</strong> 00812202110194 | <strong>ОКПО:</strong> 31290279</p>
449
- <p><strong>Юр. адрес:</strong> г. Бишкек, ул. Токольдош 3а</p>
450
- <p><strong>Банк:</strong> Центральный филиал ОАО «Бакай Банк», <strong>БИК:</strong> 124030, <strong>Счет(KGS):</strong> 1240020000834408</p>
451
  </div>
452
  </div>
453
  </section>
@@ -462,48 +450,33 @@ LANDING_TEMPLATE = '''
462
  document.addEventListener('DOMContentLoaded', function() {
463
  const header = document.querySelector('.header');
464
  const menuToggle = document.querySelector('.menu-toggle');
465
- const navLinks = document.querySelector('.nav-links');
 
466
 
467
  window.addEventListener('scroll', () => {
468
- if (window.scrollY > 50) {
469
- header.classList.add('scrolled');
470
- } else {
471
- header.classList.remove('scrolled');
472
- }
473
  });
474
 
475
- menuToggle.addEventListener('click', () => {
476
- navLinks.classList.toggle('active');
477
- });
478
-
479
- document.querySelectorAll('.nav-links a').forEach(link => {
480
- link.addEventListener('click', () => {
481
- if (navLinks.classList.contains('active')) {
482
- navLinks.classList.remove('active');
483
- }
484
- });
485
  });
486
 
487
  const filterContainer = document.querySelector('.equipment-filters');
488
  if (filterContainer) {
489
- const filterButtons = filterContainer.querySelectorAll('.filter-btn');
490
- const equipmentItems = document.querySelectorAll('.equipment-card');
491
-
492
- filterButtons.forEach(button => {
493
- button.addEventListener('click', () => {
494
- filterButtons.forEach(btn => btn.classList.remove('active'));
495
- button.classList.add('active');
496
-
497
- const filter = button.getAttribute('data-filter');
498
-
499
- equipmentItems.forEach(item => {
500
- if (filter === 'all' || item.getAttribute('data-category') === filter) {
501
- item.style.display = 'block';
502
- } else {
503
- item.style.display = 'none';
504
- }
505
  });
506
- });
507
  });
508
  }
509
  });
@@ -532,7 +505,7 @@ ADMIN_TEMPLATE = '''
532
  .section { margin-bottom: 30px; padding: 20px; background-color: #fafafa; border: 1px solid #e9e9e9; border-radius: 8px; }
533
  form { margin-bottom: 20px; }
534
  label { font-weight: 500; margin-top: 10px; display: block; color: #555; font-size: 0.9rem;}
535
- input[type="text"], input[type="number"], input[type="password"], textarea, select { width: 100%; padding: 10px 12px; margin-top: 5px; border: 1px solid #ddd; border-radius: 6px; font-size: 0.95rem; box-sizing: border-box; transition: border-color 0.3s ease; background-color: #fff; }
536
  input:focus, textarea:focus, select:focus { border-color: #8e44ad; outline: none; box-shadow: 0 0 0 2px rgba(142, 68, 173, 0.1); }
537
  textarea { min-height: 80px; resize: vertical; }
538
  input[type="file"] { padding: 8px; background-color: #ffffff; cursor: pointer; border: 1px solid #ddd;}
@@ -541,11 +514,8 @@ ADMIN_TEMPLATE = '''
541
  button:active { transform: scale(0.98); }
542
  .delete-button { background-color: #e74c3c; }
543
  .delete-button:hover { background-color: #c0392b; }
544
- .add-button { background-color: #8e44ad; }
545
- .add-button:hover { background-color: #6a0dad; }
546
  .item-list { display: grid; gap: 20px; }
547
  .item { background: #fff; padding: 15px 20px; border-radius: 8px; border: 1px solid #eee; }
548
- .item p { margin: 5px 0; font-size: 0.9rem; color: #666; }
549
  .item-actions { margin-top: 15px; display: flex; gap: 10px; flex-wrap: wrap; }
550
  .edit-form-container { margin-top: 15px; padding: 20px; background: #fdf9ff; border: 1px dashed #ddd; border-radius: 6px; display: none; }
551
  details { background-color: #fafafa; border: 1px solid #e9e9e9; border-radius: 8px; margin-bottom: 20px; }
@@ -570,209 +540,76 @@ ADMIN_TEMPLATE = '''
570
  </div>
571
 
572
  {% with messages = get_flashed_messages(with_categories=true) %}
573
- {% if messages %}
574
- {% for category, message in messages %}
575
- <div class="message {{ category }}">{{ message }}</div>
576
- {% endfor %}
577
- {% endif %}
578
  {% endwith %}
579
 
580
  <div class="section">
581
- <h2><i class="fas fa-sync-alt"></i> Синхронизация с Датацентром</h2>
582
  <div class="sync-buttons">
583
- <form method="POST" action="{{ url_for('force_upload') }}" style="display: inline;" onsubmit="return confirm('Вы уверены, что хотите принудительно загрузить локальные данные на сервер?');">
584
- <button type="submit" class="button" title="Загрузить локальные файлы на Hugging Face"><i class="fas fa-upload"></i> Загрузить БД</button>
585
  </form>
586
- <form method="POST" action="{{ url_for('force_download') }}" style="display: inline;" onsubmit="return confirm('Вы уверены, что хотите принудительно скачать данные с сервера?');">
587
- <button type="submit" class="button" title="Скачать файлы (перезапишет локальные)"><i class="fas fa-download"></i> Скачать БД</button>
588
  </form>
589
  </div>
590
  </div>
591
 
592
  <div class="section">
593
- <h2><i class="fas fa-concierge-bell"></i> Управление услугами "под ключ"</h2>
594
- <details>
595
- <summary><i class="fas fa-plus-circle"></i> Добавить новую услугу</summary>
596
- <div class="form-content">
597
- <form method="POST" enctype="multipart/form-data">
598
- <input type="hidden" name="action" value="add_service">
599
- <label for="add_service_title">Заголовок *:</label>
600
- <input type="text" id="add_service_title" name="title" required>
601
- <label for="add_service_icon">Иконка (FontAwesome, например: fas fa-tools) *:</label>
602
- <input type="text" id="add_service_icon" name="icon" placeholder="fas fa-tools" required>
603
- <label for="add_service_description">Описание *:</label>
604
- <textarea id="add_service_description" name="description" rows="3" required></textarea>
605
- <label for="add_service_photo">Фото (опционально):</label>
606
- <input type="file" id="add_service_photo" name="photo" accept="image/*">
607
- <button type="submit" class="add-button"><i class="fas fa-save"></i> Добавить услугу</button>
608
- </form>
609
- </div>
610
- </details>
611
  <h3>Список услуг:</h3>
612
- {% if services %}
613
- <div class="item-list">
614
- {% for service in services %}
615
- <div class="item">
616
- <div style="display: flex; gap: 15px; align-items: flex-start;">
617
- <div class="photo-preview" style="flex-shrink: 0;">
618
- {% if service.photo %}
619
- <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/services/{{ service.photo }}" alt="Фото">
620
- {% endif %}
621
- </div>
622
- <div style="flex-grow: 1;">
623
- <h3 style="margin-top: 0; margin-bottom: 5px; color: #333;"><i class="{{ service.icon }} fa-fw"></i> {{ service.title }}</h3>
624
- <p>{{ service.description }}</p>
625
- </div>
626
- </div>
627
- <div class="item-actions">
628
- <button type="button" class="button" onclick="toggleEditForm('edit-service-{{ loop.index0 }}')"><i class="fas fa-edit"></i> Редактировать</button>
629
- <form method="POST" style="margin:0;" onsubmit="return confirm('Удалить услугу \'{{ service.title }}\'?');">
630
- <input type="hidden" name="action" value="delete_service">
631
- <input type="hidden" name="index" value="{{ loop.index0 }}">
632
- <button type="submit" class="delete-button"><i class="fas fa-trash-alt"></i> Удалить</button>
633
- </form>
634
- </div>
635
- <div id="edit-service-{{ loop.index0 }}" class="edit-form-container">
636
- <form method="POST" enctype="multipart/form-data">
637
- <input type="hidden" name="action" value="edit_service">
638
- <input type="hidden" name="index" value="{{ loop.index0 }}">
639
- <label>Заголовок *:</label>
640
- <input type="text" name="title" value="{{ service.title }}" required>
641
- <label>Иконка *:</label>
642
- <input type="text" name="icon" value="{{ service.icon }}" required>
643
- <label>Описание *:</label>
644
- <textarea name="description" rows="3" required>{{ service.description }}</textarea>
645
- <label>Заменить фото:</label>
646
- <input type="file" name="photo" accept="image/*">
647
- <button type="submit" class="add-button"><i class="fas fa-save"></i> Сохранить</button>
648
- </form>
649
- </div>
650
- </div>
651
- {% endfor %}
652
- </div>
653
- {% else %}
654
- <p>Услуг пока нет.</p>
655
- {% endif %}
656
  </div>
657
 
658
  <div class="section">
659
  <h2><i class="fas fa-tags"></i> Категории оборудования</h2>
660
- <details>
661
- <summary><i class="fas fa-plus-circle"></i> Добавить новую категорию</summary>
662
- <div class="form-content">
663
- <form method="POST">
664
- <input type="hidden" name="action" value="add_category">
665
- <label for="add_category_name">Название новой категории:</label>
666
- <input type="text" id="add_category_name" name="category_name" required>
667
- <button type="submit" class="add-button"><i class="fas fa-plus"></i> Добавить</button>
668
- </form>
669
- </div>
670
- </details>
671
- {% if categories %}
672
- <div class="item-list">
673
- {% for category in categories %}
674
- <div class="item" style="display: flex; justify-content: space-between; align-items: center;">
675
- <span>{{ category }}</span>
676
- <form method="POST" style="margin: 0;" onsubmit="return confirm('Удалить категорию \'{{ category }}\'?');">
677
- <input type="hidden" name="action" value="delete_category">
678
- <input type="hidden" name="category_name" value="{{ category }}">
679
- <button type="submit" class="delete-button" style="padding: 5px 10px; font-size: 0.8rem; margin: 0;"><i class="fas fa-trash-alt"></i></button>
680
- </form>
681
- </div>
682
- {% endfor %}
683
- </div>
684
- {% else %}
685
- <p>Категорий пока нет.</p>
686
- {% endif %}
687
  </div>
688
 
689
  <div class="section">
690
- <h2><i class="fas fa-box-open"></i> Управление оборудованием</h2>
691
- <details>
692
- <summary><i class="fas fa-plus-circle"></i> Добавить новое оборудование</summary>
693
- <div class="form-content">
694
- <form method="POST" enctype="multipart/form-data">
695
- <input type="hidden" name="action" value="add_product">
696
- <label for="add_name">Название *:</label>
697
- <input type="text" id="add_name" name="name" required>
698
- <label for="add_price">Цена (KGS) *:</label>
699
- <input type="number" id="add_price" name="price" step="0.01" min="0" required>
700
- <label for="add_category">Категория:</label>
701
- <select id="add_category" name="category">
702
- <option value="Без категории">Без категории</option>
703
- {% for category in categories %}
704
- <option value="{{ category }}">{{ category }}</option>
705
- {% endfor %}
706
- </select>
707
- <label for="add_photos">Фотографии (до 10 шт.):</label>
708
- <input type="file" id="add_photos" name="photos" accept="image/*" multiple>
709
- <button type="submit" class="add-button"><i class="fas fa-save"></i> Добавить</button>
710
- </form>
711
- </div>
712
- </details>
713
-
714
  <h3>Список оборудования:</h3>
715
- {% if products %}
716
- <div class="item-list">
717
- {% for product in products %}
718
- <div class="item">
719
- <div style="display: flex; gap: 15px; align-items: flex-start;">
720
- <div class="photo-preview" style="flex-shrink: 0;">
721
- {% if product.get('photos') %}
722
- <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product['photos'][0] }}" alt="Фото">
723
- {% else %}
724
- <img src="https://via.placeholder.com/70x70.png?text=N/A" alt="Нет фото">
725
- {% endif %}
726
- </div>
727
- <div style="flex-grow: 1;">
728
- <h3 style="margin-top: 0; margin-bottom: 5px; color: #333;">{{ product['name'] }}</h3>
729
- <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
730
- <p><strong>Цена:</strong> {{ "%.2f"|format(product['price']) }} KGS</p>
731
- </div>
732
- </div>
733
- <div class="item-actions">
734
- <button type="button" class="button" onclick="toggleEditForm('edit-form-{{ loop.index0 }}')"><i class="fas fa-edit"></i> Редактировать</button>
735
- <form method="POST" style="margin:0;" onsubmit="return confirm('Удалить \'{{ product['name'] }}\'?');">
736
- <input type="hidden" name="action" value="delete_product">
737
- <input type="hidden" name="index" value="{{ loop.index0 }}">
738
- <button type="submit" class="delete-button"><i class="fas fa-trash-alt"></i> Удалить</button>
739
- </form>
740
- </div>
741
-
742
- <div id="edit-form-{{ loop.index0 }}" class="edit-form-container">
743
- <form method="POST" enctype="multipart/form-data">
744
- <input type="hidden" name="action" value="edit_product">
745
- <input type="hidden" name="index" value="{{ loop.index0 }}">
746
- <label>Название *:</label>
747
- <input type="text" name="name" value="{{ product['name'] }}" required>
748
- <label>Цена (KGS) *:</label>
749
- <input type="number" name="price" step="0.01" min="0" value="{{ product['price'] }}" required>
750
- <label>Категория:</label>
751
- <select name="category">
752
- <option value="Без категории">Без категории</option>
753
- {% for category in categories %}
754
- <option value="{{ category }}" {% if product.get('category') == category %}selected{% endif %}>{{ category }}</option>
755
- {% endfor %}
756
- </select>
757
- <label>Заменить фотографии:</label>
758
- <input type="file" name="photos" accept="image/*" multiple>
759
- <button type="submit" class="add-button"><i class="fas fa-save"></i> Сохранить</button>
760
- </form>
761
- </div>
762
- </div>
763
- {% endfor %}
764
- </div>
765
- {% else %}
766
- <p>Оборудования пока нет.</p>
767
- {% endif %}
768
  </div>
769
  </div>
770
  <script>
771
  function toggleEditForm(formId) {
772
- const formContainer = document.getElementById(formId);
773
- if (formContainer) {
774
- formContainer.style.display = formContainer.style.display === 'none' || formContainer.style.display === '' ? 'block' : 'none';
775
- }
776
  }
777
  </script>
778
  </body>
@@ -787,6 +624,7 @@ def landing():
787
  services=data.get('services', []),
788
  products=data.get('products', []),
789
  categories=sorted(data.get('categories', [])),
 
790
  repo_id=REPO_ID,
791
  contact_phone=CONTACT_PHONE,
792
  whatsapp_phone=WHATSAPP_PHONE,
@@ -796,132 +634,120 @@ def landing():
796
  @app.route('/admin', methods=['GET', 'POST'])
797
  def admin():
798
  data = load_data()
799
- products = data.get('products', [])
800
- categories = data.get('categories', [])
801
- services = data.get('services', [])
802
 
803
  if request.method == 'POST':
804
  action = request.form.get('action')
805
- logging.info(f"Admin action received: {action}")
806
  try:
807
  if action == 'add_category':
808
  category_name = request.form.get('category_name', '').strip()
809
- if category_name and category_name not in categories:
810
- categories.append(category_name)
811
- data['categories'] = categories
812
  save_data(data)
813
  flash(f"Категория '{category_name}' добавлена.", 'success')
814
  else:
815
- flash(f"Категория '{category_name}' уже существует или пуста.", 'error')
816
 
817
  elif action == 'delete_category':
818
  category_to_delete = request.form.get('category_name')
819
- if category_to_delete in categories:
820
- categories.remove(category_to_delete)
821
- for p in products:
822
- if p.get('category') == category_to_delete:
823
- p['category'] = 'Без категории'
824
- data['categories'] = categories
825
- data['products'] = products
826
  save_data(data)
827
  flash(f"Категория '{category_to_delete}' удалена.", 'success')
 
 
 
 
 
 
 
 
828
  else:
829
- flash("Не удалось удалить категорию.", 'error')
830
 
831
- elif action == 'add_product':
832
- name = request.form.get('name', '').strip()
833
- price_str = request.form.get('price', '').replace(',', '.')
834
- category = request.form.get('category')
835
- photos_files = request.files.getlist('photos')
836
- if not name or not price_str:
837
- flash("Название и цена обязательны.", 'error')
838
- return redirect(url_for('admin'))
839
- try:
840
- price = round(float(price_str), 2)
841
- if price < 0: price = 0
842
- except ValueError:
843
- flash("Неверный формат цены.", 'error')
844
- return redirect(url_for('admin'))
845
- photos_list = upload_photos_to_hf(photos_files, name, 'photos')
846
- new_product = {'name': name, 'price': price, 'category': category, 'photos': photos_list}
847
- products.append(new_product)
848
- data['products'] = products
849
  save_data(data)
850
- flash(f"Оборудование '{name}' добавлено.", 'success')
851
 
852
- elif action == 'edit_product':
853
- index = int(request.form.get('index'))
854
- product_to_edit = products[index]
855
- product_to_edit['name'] = request.form.get('name', '').strip()
856
- price_str = request.form.get('price', '').replace(',', '.')
857
- product_to_edit['category'] = request.form.get('category')
858
- try:
859
- product_to_edit['price'] = round(float(price_str), 2)
860
- except ValueError:
861
- flash("Неверный формат цены при редактировании.", 'warning')
862
- photos_files = request.files.getlist('photos')
863
- if photos_files and any(f.filename for f in photos_files):
864
- new_photos = upload_photos_to_hf(photos_files, product_to_edit['name'], 'photos')
865
- if new_photos:
866
- delete_photos_from_hf(product_to_edit.get('photos', []), 'photos')
867
- product_to_edit['photos'] = new_photos
868
- products[index] = product_to_edit
869
- data['products'] = products
870
  save_data(data)
871
- flash(f"Оборудование '{product_to_edit['name']}' обновлено.", 'success')
872
 
873
- elif action == 'delete_product':
874
- index = int(request.form.get('index'))
875
- deleted_product = products.pop(index)
876
- delete_photos_from_hf(deleted_product.get('photos', []), 'photos')
877
- data['products'] = products
 
 
 
 
 
 
 
 
 
 
 
 
 
 
878
  save_data(data)
879
- flash(f"Оборудование '{deleted_product.get('name')}' удалено.", 'success')
880
 
881
- elif action == 'add_service':
882
- title = request.form.get('title', '').strip()
883
- icon = request.form.get('icon', 'fas fa-check').strip()
884
- description = request.form.get('description', '').strip()
885
- photo_file = request.files.get('photo')
886
- if not all([title, icon, description]):
887
- flash("Все поля услуги обязательны.", 'error')
888
- return redirect(url_for('admin'))
889
- photo_list = upload_photos_to_hf([photo_file], title, 'services')
890
- new_service = {'title': title, 'icon': icon, 'description': description, 'photo': photo_list[0] if photo_list else None}
891
- services.append(new_service)
892
- data['services'] = services
893
  save_data(data)
894
- flash(f"Услуга '{title}' добавлена.", 'success')
895
-
896
- elif action == 'edit_service':
897
- index = int(request.form.get('index'))
898
- service_to_edit = services[index]
899
- service_to_edit['title'] = request.form.get('title', '').strip()
900
- service_to_edit['icon'] = request.form.get('icon', '').strip()
901
- service_to_edit['description'] = request.form.get('description', '').strip()
902
- photo_file = request.files.get('photo')
903
- if photo_file and photo_file.filename:
904
- new_photo = upload_photos_to_hf([photo_file], service_to_edit['title'], 'services')
905
- if new_photo:
906
- delete_photos_from_hf([service_to_edit.get('photo')], 'services')
907
- service_to_edit['photo'] = new_photo[0]
908
- services[index] = service_to_edit
909
- data['services'] = services
 
 
 
 
 
910
  save_data(data)
911
- flash(f"Услуга '{service_to_edit['title']}' обновлена.", 'success')
912
 
913
- elif action == 'delete_service':
914
- index = int(request.form.get('index'))
915
- deleted_service = services.pop(index)
916
- delete_photos_from_hf([deleted_service.get('photo')], 'services')
917
- data['services'] = services
918
  save_data(data)
919
- flash(f"Услуга '{deleted_service.get('title')}' удалена.", 'success')
920
 
921
  return redirect(url_for('admin'))
922
  except Exception as e:
923
  logging.error(f"Admin action '{action}' failed: {e}", exc_info=True)
924
- flash(f"Произошла ошибка при выполнении действия '{action}'.", 'error')
925
  return redirect(url_for('admin'))
926
 
927
  return render_template_string(
@@ -929,92 +755,61 @@ def admin():
929
  products=data.get('products', []),
930
  categories=sorted(data.get('categories', [])),
931
  services=data.get('services', []),
 
932
  repo_id=REPO_ID
933
  )
934
 
935
  def upload_photos_to_hf(photo_files, item_name, folder):
936
  if not photo_files or not HF_TOKEN_WRITE:
937
- if photo_files and any(f and f.filename for f in photo_files):
938
- flash("HF_TOKEN (write) не настроен. Фото не загружены.", "warning")
939
  return []
940
-
941
- api = HfApi()
942
- uploaded_photos = []
943
- uploads_dir = 'uploads_temp'
944
- os.makedirs(uploads_dir, exist_ok=True)
945
-
946
  for photo in photo_files:
947
  if photo and photo.filename:
948
  try:
949
  ext = os.path.splitext(photo.filename)[1].lower()
950
- if ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']:
951
- continue
952
  safe_name = secure_filename(item_name.replace(' ', '_'))[:50]
953
- photo_filename = f"{safe_name}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{ext}"
954
- temp_path = os.path.join(uploads_dir, photo_filename)
955
  photo.save(temp_path)
956
-
957
- api.upload_file(
958
- path_or_fileobj=temp_path,
959
- path_in_repo=f"{folder}/{photo_filename}",
960
- repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE
961
- )
962
- uploaded_photos.append(photo_filename)
963
  os.remove(temp_path)
964
  except Exception as e:
965
- logging.error(f"Error uploading photo {photo.filename}: {e}", exc_info=True)
966
- flash(f"Ошибка загрузки фото {photo.filename}.", 'error')
967
-
968
  return uploaded_photos
969
 
970
  def delete_photos_from_hf(photo_list, folder):
971
- if not photo_list or not HF_TOKEN_WRITE:
972
- return
973
-
974
- photos_to_delete = [p for p in photo_list if p]
975
- if not photos_to_delete:
976
- return
977
-
978
  try:
979
- api = HfApi()
980
- api.delete_files(
981
- repo_id=REPO_ID,
982
- paths_in_repo=[f"{folder}/{p}" for p in photos_to_delete],
983
- repo_type="dataset", token=HF_TOKEN_WRITE
984
- )
985
- logging.info(f"Deleted photos from HF: {photos_to_delete}")
986
  except Exception as e:
987
- logging.error(f"Error deleting photos from HF: {e}", exc_info=True)
988
  flash("Не удалось удалить старые фото с сервера.", "warning")
989
 
990
  @app.route('/force_upload', methods=['POST'])
991
  def force_upload():
992
- logging.info("Forcing upload to Hugging Face...")
993
- try:
994
- upload_db_to_hf()
995
- flash("Данные успешно загружены на Hugging Face.", 'success')
996
- except Exception as e:
997
- flash(f"Ошибка при загрузке на Hugging Face: {e}", 'error')
998
  return redirect(url_for('admin'))
999
 
1000
  @app.route('/force_download', methods=['POST'])
1001
  def force_download():
1002
- logging.info("Forcing download from Hugging Face...")
1003
- try:
1004
- if download_db_from_hf():
1005
- flash("Данные успешно скачаны. Локальные файлы обновлены.", 'success')
1006
- else:
1007
- flash("Не удалось скачать данные.", 'error')
1008
- except Exception as e:
1009
- flash(f"Ошибка при скачивании с Hugging Face: {e}", 'error')
1010
  return redirect(url_for('admin'))
1011
 
1012
  if __name__ == '__main__':
1013
- logging.info("Application starting up...")
1014
  download_db_from_hf()
1015
- load_data()
1016
  if HF_TOKEN_WRITE:
1017
- backup_thread = threading.Thread(target=periodic_backup, daemon=True)
1018
- backup_thread.start()
1019
  port = int(os.environ.get('PORT', 7860))
1020
  app.run(debug=False, host='0.0.0.0', port=port)
 
15
  load_dotenv()
16
 
17
  app = Flask(__name__)
18
+ app.secret_key = 'raina_hvac_secret_key_98765_landing_v2'
19
  DATA_FILE = 'data.json'
20
 
21
  SYNC_FILES = [DATA_FILE]
 
67
  try:
68
  if file_name == DATA_FILE:
69
  with open(file_name, 'w', encoding='utf-8') as f:
70
+ json.dump({'products': [], 'categories': [], 'services': [], 'projects': []}, f)
71
  logging.info(f"Created empty local file {file_name} because it was not found on HF.")
72
  except Exception as create_e:
73
  logging.error(f"Failed to create empty local file {file_name}: {create_e}")
 
125
  logging.info("Periodic backup finished.")
126
 
127
  def load_data():
128
+ default_data = {'products': [], 'categories': [], 'services': [], 'projects': []}
129
  try:
130
  with open(DATA_FILE, 'r', encoding='utf-8') as file:
131
  data = json.load(file)
132
  logging.info(f"Local data loaded successfully from {DATA_FILE}")
133
  if not isinstance(data, dict):
 
134
  raise FileNotFoundError
135
  if 'products' not in data: data['products'] = []
136
  if 'categories' not in data: data['categories'] = []
137
  if 'services' not in data: data['services'] = []
138
+ if 'projects' not in data: data['projects'] = []
139
  return data
140
  except (FileNotFoundError, json.JSONDecodeError):
141
  logging.warning(f"Local file {DATA_FILE} not found or corrupt. Attempting download from HF.")
 
148
  if 'products' not in data: data['products'] = []
149
  if 'categories' not in data: data['categories'] = []
150
  if 'services' not in data: data['services'] = []
151
+ if 'projects' not in data: data['projects'] = []
152
  return data
153
  except Exception as e:
154
  logging.error(f"Unknown error loading downloaded {DATA_FILE}: {e}. Using default.", exc_info=True)
 
172
  if 'products' not in data: data['products'] = []
173
  if 'categories' not in data: data['categories'] = []
174
  if 'services' not in data: data['services'] = []
175
+ if 'projects' not in data: data['projects'] = []
176
  with open(DATA_FILE, 'w', encoding='utf-8') as file:
177
  json.dump(data, file, ensure_ascii=False, indent=4)
178
  logging.info(f"Data successfully saved to {DATA_FILE}")
 
208
  h1 { font-size: clamp(2.5rem, 6vw, 4rem); }
209
  h2 { font-size: clamp(2rem, 5vw, 3rem); text-align: center; margin-bottom: 60px; position: relative; }
210
  h2::after { content: ''; display: block; width: 80px; height: 4px; background: linear-gradient(90deg, var(--primary-color), var(--secondary-color)); margin: 15px auto 0; border-radius: 2px; }
211
+ h3 { font-size: clamp(1.2rem, 3vw, 1.5rem); color: var(--primary-color); margin-bottom: 15px; }
212
  p { margin-bottom: 1rem; color: var(--text-muted); }
213
  .btn { display: inline-block; padding: 12px 28px; background: linear-gradient(90deg, var(--primary-color), var(--secondary-color)); color: #fff; border-radius: 50px; text-decoration: none; font-weight: 600; transition: all 0.3s ease; box-shadow: 0 4px 15px var(--accent-glow); }
214
  .btn:hover { transform: translateY(-3px) scale(1.05); box-shadow: 0 8px 25px var(--accent-glow); }
 
217
  .header.scrolled { padding: 10px 0; box-shadow: 0 2px 10px rgba(0,0,0,0.3); }
218
  .navbar { display: flex; justify-content: space-between; align-items: center; }
219
  .logo { font-size: 1.8rem; font-weight: 700; color: #fff; text-decoration: none; }
220
+ .nav-links { display: none; list-style: none; }
221
+ .menu-toggle { display: block; font-size: 1.5rem; cursor: pointer; color: #fff; }
 
 
222
 
223
+ #hero { min-height: 100vh; display: flex; align-items: center; background-image: linear-gradient(rgba(18, 18, 28, 0.7), rgba(18, 18, 28, 1)), url(https://i.imgur.com/k6O6XbJ.jpeg); background-size: cover; background-position: center; }
224
  .hero-content { text-align: center; max-width: 800px; margin: 0 auto; }
225
+ .hero-content p { font-size: clamp(1rem, 2.5vw, 1.2rem); margin: 30px 0; max-width: 600px; margin-left: auto; margin-right: auto;}
226
 
227
+ .grid-2 { display: grid; grid-template-columns: 1fr; gap: 40px; align-items: center; }
228
+ .about-img { width: 100%; border-radius: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.4); max-width: 500px; margin: 0 auto 30px auto;}
229
 
230
+ .services-grid, .projects-grid { display: grid; grid-template-columns: 1fr; gap: 30px; }
231
+ .service-card, .turnkey-card { background-color: var(--card-bg); padding: 30px; border-radius: 15px; border: 1px solid #2a2a4a; transition: all 0.3s ease; }
232
+ .service-card:hover, .turnkey-card:hover { transform: translateY(-5px); border-color: var(--primary-color); box-shadow: 0 8px 25px var(--accent-glow); }
233
  .service-card i { font-size: 2.5rem; color: var(--primary-color); margin-bottom: 20px; }
234
+
235
+ .turnkey-card { display: flex; flex-direction: column; }
236
+ .turnkey-img { width: 100%; height: 200px; object-fit: cover; border-radius: 10px 10px 0 0;}
 
237
  .turnkey-content { padding: 30px; flex-grow: 1;}
238
 
239
+ .equipment-filters { display: flex; justify-content: center; flex-wrap: wrap; gap: 10px; margin-bottom: 40px; }
240
+ .filter-btn { padding: 8px 18px; border: 1px solid var(--primary-color); background-color: transparent; color: var(--primary-color); border-radius: 20px; cursor: pointer; transition: all 0.3s; font-size: 0.9rem;}
241
  .filter-btn.active, .filter-btn:hover { background-color: var(--primary-color); color: #fff; }
242
+ .equipment-grid { display: grid; grid-template-columns: 1fr; gap: 30px; }
243
  .equipment-card { background-color: var(--card-bg); border-radius: 15px; overflow: hidden; text-align: center; padding: 20px; border: 1px solid #2a2a4a; transition: all 0.3s ease; }
244
  .equipment-card img { width: 100%; height: 180px; object-fit: contain; margin-bottom: 15px; }
245
  .equipment-card h3 { font-size: 1.2rem; }
246
  .equipment-card .price { font-size: 1.3rem; font-weight: 700; color: #fff; margin: 10px 0; }
247
 
248
+ .project-card { position: relative; border-radius: 15px; overflow: hidden; aspect-ratio: 4 / 3; }
 
249
  .project-card img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform 0.4s ease; }
250
  .project-overlay { position: absolute; bottom: 0; left: 0; right: 0; background: linear-gradient(to top, rgba(18,18,28,1) 0%, rgba(18,18,28,0) 100%); padding: 40px 20px 20px; transition: all 0.4s ease; }
251
+ .project-card h3 { margin-bottom: 5px; font-size: clamp(1.1rem, 4vw, 1.3rem); }
252
+ .project-card p { margin-bottom: 0; transition: opacity 0.4s ease, max-height 0.4s ease; opacity: 0; max-height: 0; overflow: hidden; }
253
  .project-card:hover img { transform: scale(1.05); }
 
254
  .project-card:hover p { opacity: 1; max-height: 200px; }
255
 
256
  #contact { background-color: var(--card-bg); }
 
258
  .contact-info { margin-top: 40px; display: flex; flex-direction: column; align-items: center; gap: 20px; }
259
  .contact-info p { font-size: 1.2rem; margin-bottom: 0; }
260
  .contact-info a { color: var(--primary-color); text-decoration: none; font-weight: 600; }
 
261
  .footer { text-align: center; padding: 30px 0; background-color: #0d0d14; }
262
 
263
+ @media (min-width: 576px) {
264
+ .services-grid { grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }
265
+ .equipment-grid { grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); }
266
+ .projects-grid { grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }
267
  }
268
+ @media (min-width: 992px) {
269
+ .nav-links { display: flex; gap: 30px; }
270
+ .menu-toggle { display: none; }
271
+ .grid-2 { grid-template-columns: 1fr 1fr; }
272
+ .about-img { margin: 0; }
 
273
  }
274
+ .mobile-nav {
275
+ position: fixed;
276
+ top: 0;
277
+ left: -100%;
278
+ width: 80%;
279
+ max-width: 300px;
280
+ height: 100vh;
281
+ background-color: var(--card-bg);
282
+ box-shadow: 5px 0 15px rgba(0,0,0,0.2);
283
+ z-index: 1001;
284
+ transition: left 0.4s ease-in-out;
285
+ display: flex;
286
+ flex-direction: column;
287
+ justify-content: center;
288
+ align-items: center;
289
+ padding: 20px;
290
+ }
291
+ .mobile-nav.active { left: 0; }
292
+ .mobile-nav .nav-links { display: flex; flex-direction: column; align-items: center; gap: 25px; padding: 0; }
293
+ .mobile-nav .nav-links a { font-size: 1.2rem; }
294
+ #nav-close-btn { position: absolute; top: 20px; right: 20px; font-size: 1.8rem; color: #fff; cursor: pointer; }
295
  </style>
296
  </head>
297
  <body>
298
  <header class="header">
299
  <div class="container navbar">
300
  <a href="#" class="logo">Раина</a>
301
+ <nav class="nav-links">
302
+ <a href="#about">О компании</a>
303
+ <a href="#services">Услуги</a>
304
+ <a href="#turnkey">Под ключ</a>
305
+ <a href="#equipment">Оборудование</a>
306
+ <a href="#projects">Проекты</a>
307
+ <a href="#contact">Контакты</a>
308
+ </nav>
309
  <div class="menu-toggle"><i class="fas fa-bars"></i></div>
310
  </div>
311
  </header>
312
 
313
+ <div class="mobile-nav">
314
+ <div id="nav-close-btn"><i class="fas fa-times"></i></div>
315
+ <ul class="nav-links">
316
+ <li><a href="#about">О компании</a></li>
317
+ <li><a href="#services">Услуги</a></li>
318
+ <li><a href="#turnkey">Под ключ</a></li>
319
+ <li><a href="#equipment">Оборудование</a></li>
320
+ <li><a href="#projects">Проекты</a></li>
321
+ <li><a href="#contact">Контакты</a></li>
322
+ </ul>
323
+ </div>
324
+
325
  <section id="hero">
326
  <div class="container hero-content">
327
  <h1>ОсОО "Раина": Ваш Партнер в Вентиляции и Кондиционировании</h1>
 
337
  <img src="https://i.imgur.com/8QhV42S.jpeg" alt="Команда Раина" class="about-img">
338
  <div>
339
  <h3>Основание и История</h3>
340
+ <p>Компания "Раина" была основана в 2009 году. За эти годы мы зарекомендовали себя как надежный партнер, стремящийся к инновациям и совершенству в области климатических решений.</p>
341
  <h3>Наша Миссия</h3>
342
+ <p>Наша миссия — создание оптимального микроклимата для наших клиентов, обеспечивающего комфорт, здоровье и высокую производительность.</p>
343
  <h3>Профессиональная Команда</h3>
344
+ <p>Наша команда состоит из высококвалифицированных и сертифицированных инженеров и техников, обладающих глубокими знаниями и опытом в области HVAC.</p>
345
  </div>
346
  </div>
347
  </div>
 
351
  <div class="container">
352
  <h2>Наши Услуги</h2>
353
  <div class="services-grid">
354
+ <div class="service-card"><i class="fas fa-drafting-compass"></i><h3>Проектирование</h3><p>Точные расчеты, 3D-модели и вся необходимая проектная документация.</p></div>
355
+ <div class="service-card"><i class="fas fa-tools"></i><h3>Монтаж</h3><p>Профессиональная установка всех типов систем HVAC, от бытовых до промышленных.</p></div>
356
+ <div class="service-card"><i class="fas fa-headset"></i><h3>Сервис 24/7</h3><p>Полный спектр услуг по плановому обслуживанию и аварийному ремонту.</p></div>
357
+ <div class="service-card"><i class="fas fa-sync-alt"></i><h3>Модернизация</h3><p>Повышение энергоэффективности и снижение расходов на эксплуатацию.</p></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
358
  </div>
359
  </div>
360
  </section>
361
 
362
+ <section id="turnkey">
363
  <div class="container">
364
  <h2>Услуги "под ключ"</h2>
365
  {% if services %}
366
  <div class="services-grid">
367
  {% for service in services %}
368
  <div class="turnkey-card">
369
+ {% if service.photo %}<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/services/{{ service.photo }}" alt="{{ service.title }}" class="turnkey-img">{% endif %}
 
 
370
  <div class="turnkey-content">
371
  <h3><i class="{{ service.icon }} fa-fw" style="margin-right: 8px; color: var(--primary-color);"></i>{{ service.title }}</h3>
372
  <p>{{ service.description }}</p>
 
375
  {% endfor %}
376
  </div>
377
  {% else %}
378
+ <p style="text-align: center;">Информация об услугах "под ключ" скоро появится.</p>
379
  {% endif %}
380
  </div>
381
  </section>
382
 
383
+ <section id="equipment" style="background-color: var(--card-bg);">
384
  <div class="container">
385
  <h2>Наше Оборудование</h2>
386
  {% if products %}
387
  <div class="equipment-filters">
388
  <button class="filter-btn active" data-filter="all">Все</button>
389
+ {% for category in categories %}<button class="filter-btn" data-filter="{{ category }}">{{ category }}</button>{% endfor %}
 
 
390
  </div>
391
  <div class="equipment-grid">
392
  {% for product in products %}
393
  <div class="equipment-card" data-category="{{ product.get('category', 'all') }}">
394
+ <img src="{% if product.photos %}https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product.photos[0] }}{% else %}https://via.placeholder.com/250x180.png?text=No+Image{% endif %}" alt="{{ product.name }}">
 
 
 
 
395
  <h3>{{ product.name }}</h3>
396
  <p class="price">{{ "%.2f"|format(product.price) }} KGS</p>
397
  <a href="https://api.whatsapp.com/send?phone={{ whatsapp_phone }}&text=Здравствуйте, интересует оборудование: {{ product.name }}" target="_blank" class="btn" style="padding: 8px 20px; font-size: 0.9rem;">Запросить</a>
 
403
  {% endif %}
404
  </div>
405
  </section>
406
+
407
  <section id="projects">
408
  <div class="container">
409
  <h2>Реализованные Проекты</h2>
410
+ {% if projects %}
411
  <div class="projects-grid">
412
+ {% for project in projects %}
413
  <div class="project-card">
414
+ <img src="{% if project.photo %}https://huggingface.co/datasets/{{ repo_id }}/resolve/main/projects/{{ project.photo }}{% else %}https://i.imgur.com/k6O6XbJ.jpeg{% endif %}" alt="{{ project.title }}">
415
  <div class="project-overlay">
416
+ <h3>{{ project.title }}</h3>
417
+ <p>{{ project.description }}</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
418
  </div>
419
  </div>
420
+ {% endfor %}
421
  </div>
422
+ {% else %}
423
+ <p style="text-align: center;">Информация о реализованных проектах скоро появится.</p>
424
+ {% endif %}
425
  </div>
426
  </section>
427
 
428
  <section id="contact">
429
  <div class="container contact-content">
430
  <h2>Контакты и Следующие Шаги</h2>
431
+ <p>Мы готовы стать вашим надежным партнером в создании идеального климата.</p>
432
  <div class="contact-info">
433
+ <p><strong>Свяжитесь с нами:</strong> <a href="tel:{{ contact_phone }}">{{ contact_phone }}</a></p>
434
  <a href="https://api.whatsapp.com/send?phone={{ whatsapp_phone }}&text=Здравствуйте, я хотел(а) бы получить консультацию по вашим услугам." target="_blank" class="btn"><i class="fab fa-whatsapp"></i> Написать в WhatsApp</a>
435
  </div>
436
  <div style="margin-top: 40px; font-size: 0.9rem; color: var(--text-muted);">
437
+ <p><strong>Реквизиты:</strong> ОсОО «Раина», ИНН: 00812202110194, ОКПО: 31290279</p>
438
+ <p><strong>Банк:</strong> Центральный филиал ОАО «Бакай Банк», БИК: 124030, Счет(KGS): 1240020000834408</p>
 
 
439
  </div>
440
  </div>
441
  </section>
 
450
  document.addEventListener('DOMContentLoaded', function() {
451
  const header = document.querySelector('.header');
452
  const menuToggle = document.querySelector('.menu-toggle');
453
+ const mobileNav = document.querySelector('.mobile-nav');
454
+ const navCloseBtn = document.getElementById('nav-close-btn');
455
 
456
  window.addEventListener('scroll', () => {
457
+ header.classList.toggle('scrolled', window.scrollY > 50);
 
 
 
 
458
  });
459
 
460
+ const closeNav = () => mobileNav.classList.remove('active');
461
+ const openNav = () => mobileNav.classList.add('active');
462
+
463
+ menuToggle.addEventListener('click', openNav);
464
+ navCloseBtn.addEventListener('click', closeNav);
465
+ mobileNav.querySelectorAll('a').forEach(link => {
466
+ link.addEventListener('click', closeNav);
 
 
 
467
  });
468
 
469
  const filterContainer = document.querySelector('.equipment-filters');
470
  if (filterContainer) {
471
+ filterContainer.addEventListener('click', (e) => {
472
+ if (e.target.classList.contains('filter-btn')) {
473
+ filterContainer.querySelector('.active').classList.remove('active');
474
+ e.target.classList.add('active');
475
+ const filter = e.target.getAttribute('data-filter');
476
+ document.querySelectorAll('.equipment-card').forEach(item => {
477
+ item.style.display = (filter === 'all' || item.getAttribute('data-category') === filter) ? 'block' : 'none';
 
 
 
 
 
 
 
 
 
478
  });
479
+ }
480
  });
481
  }
482
  });
 
505
  .section { margin-bottom: 30px; padding: 20px; background-color: #fafafa; border: 1px solid #e9e9e9; border-radius: 8px; }
506
  form { margin-bottom: 20px; }
507
  label { font-weight: 500; margin-top: 10px; display: block; color: #555; font-size: 0.9rem;}
508
+ input[type="text"], input[type="number"], textarea, select { width: 100%; padding: 10px 12px; margin-top: 5px; border: 1px solid #ddd; border-radius: 6px; font-size: 0.95rem; box-sizing: border-box; transition: border-color 0.3s ease; background-color: #fff; }
509
  input:focus, textarea:focus, select:focus { border-color: #8e44ad; outline: none; box-shadow: 0 0 0 2px rgba(142, 68, 173, 0.1); }
510
  textarea { min-height: 80px; resize: vertical; }
511
  input[type="file"] { padding: 8px; background-color: #ffffff; cursor: pointer; border: 1px solid #ddd;}
 
514
  button:active { transform: scale(0.98); }
515
  .delete-button { background-color: #e74c3c; }
516
  .delete-button:hover { background-color: #c0392b; }
 
 
517
  .item-list { display: grid; gap: 20px; }
518
  .item { background: #fff; padding: 15px 20px; border-radius: 8px; border: 1px solid #eee; }
 
519
  .item-actions { margin-top: 15px; display: flex; gap: 10px; flex-wrap: wrap; }
520
  .edit-form-container { margin-top: 15px; padding: 20px; background: #fdf9ff; border: 1px dashed #ddd; border-radius: 6px; display: none; }
521
  details { background-color: #fafafa; border: 1px solid #e9e9e9; border-radius: 8px; margin-bottom: 20px; }
 
540
  </div>
541
 
542
  {% with messages = get_flashed_messages(with_categories=true) %}
543
+ {% if messages %}{% for category, message in messages %}<div class="message {{ category }}">{{ message }}</div>{% endfor %}{% endif %}
 
 
 
 
544
  {% endwith %}
545
 
546
  <div class="section">
547
+ <h2><i class="fas fa-sync-alt"></i> Синхронизация</h2>
548
  <div class="sync-buttons">
549
+ <form method="POST" action="{{ url_for('force_upload') }}" style="display: inline;" onsubmit="return confirm('Загрузить локальные данные на сервер?');">
550
+ <button type="submit" class="button"><i class="fas fa-upload"></i> Загрузить БД</button>
551
  </form>
552
+ <form method="POST" action="{{ url_for('force_download') }}" style="display: inline;" onsubmit="return confirm('Скачать данные с сервера (перезапишет локальные)?');">
553
+ <button type="submit" class="button"><i class="fas fa-download"></i> Скачать БД</button>
554
  </form>
555
  </div>
556
  </div>
557
 
558
  <div class="section">
559
+ <h2><i class="fas fa-concierge-bell"></i> Услуги "под ключ"</h2>
560
+ <details><summary><i class="fas fa-plus-circle"></i> Добавить услугу</summary><div class="form-content">
561
+ <form method="POST" enctype="multipart/form-data">
562
+ <input type="hidden" name="action" value="add_service"><label for="add_service_title">Заголовок *:</label><input type="text" id="add_service_title" name="title" required><label for="add_service_icon">Иконка (FontAwesome) *:</label><input type="text" id="add_service_icon" name="icon" placeholder="fas fa-tools" required><label for="add_service_description">Описание *:</label><textarea id="add_service_description" name="description" rows="3" required></textarea><label for="add_service_photo">Фото:</label><input type="file" id="add_service_photo" name="photo" accept="image/*"><button type="submit"><i class="fas fa-save"></i> Добавить</button>
563
+ </form>
564
+ </div></details>
 
 
 
 
 
 
 
 
 
 
 
 
565
  <h3>Список услуг:</h3>
566
+ {% if services %}<div class="item-list">{% for service in services %}<div class="item">
567
+ <div style="display: flex; gap: 15px; align-items: flex-start;"><div class="photo-preview">{% if service.photo %}<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/services/{{ service.photo }}" alt="Фото">{% endif %}</div><div style="flex-grow: 1;"><h3 style="margin: 0 0 5px;"><i class="{{ service.icon }} fa-fw"></i> {{ service.title }}</h3><p>{{ service.description }}</p></div></div>
568
+ <div class="item-actions"><button type="button" onclick="toggleEditForm('edit-service-{{ loop.index0 }}')"><i class="fas fa-edit"></i> Редактировать</button><form method="POST" style="margin:0;" onsubmit="return confirm('Удалить услугу \'{{ service.title }}\'?');"><input type="hidden" name="action" value="delete_service"><input type="hidden" name="index" value="{{ loop.index0 }}"><button type="submit" class="delete-button"><i class="fas fa-trash-alt"></i> Удалить</button></form></div>
569
+ <div id="edit-service-{{ loop.index0 }}" class="edit-form-container"><form method="POST" enctype="multipart/form-data"><input type="hidden" name="action" value="edit_service"><input type="hidden" name="index" value="{{ loop.index0 }}"><label>Заголовок *:</label><input type="text" name="title" value="{{ service.title }}" required><label>Иконка *:</label><input type="text" name="icon" value="{{ service.icon }}" required><label>Описание *:</label><textarea name="description" rows="3" required>{{ service.description }}</textarea><label>Заменить фото:</label><input type="file" name="photo" accept="image/*"><button type="submit"><i class="fas fa-save"></i> Сохранить</button></form></div>
570
+ </div>{% endfor %}</div>{% else %}<p>Услуг пока нет.</p>{% endif %}
571
+ </div>
572
+
573
+ <div class="section">
574
+ <h2><i class="fas fa-building"></i> Реализованные проекты</h2>
575
+ <details><summary><i class="fas fa-plus-circle"></i> Добавить проект</summary><div class="form-content">
576
+ <form method="POST" enctype="multipart/form-data">
577
+ <input type="hidden" name="action" value="add_project"><label for="add_project_title">Название проекта *:</label><input type="text" id="add_project_title" name="title" required><label for="add_project_description">Краткое описание *:</label><textarea id="add_project_description" name="description" rows="2" required></textarea><label for="add_project_photo">Фото *:</label><input type="file" id="add_project_photo" name="photo" accept="image/*" required><button type="submit"><i class="fas fa-save"></i> Добавить</button>
578
+ </form>
579
+ </div></details>
580
+ <h3>Список проектов:</h3>
581
+ {% if projects %}<div class="item-list">{% for project in projects %}<div class="item">
582
+ <div style="display: flex; gap: 15px; align-items: flex-start;"><div class="photo-preview">{% if project.photo %}<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/projects/{{ project.photo }}" alt="Фото">{% endif %}</div><div style="flex-grow: 1;"><h3 style="margin: 0 0 5px;">{{ project.title }}</h3><p>{{ project.description }}</p></div></div>
583
+ <div class="item-actions"><button type="button" onclick="toggleEditForm('edit-project-{{ loop.index0 }}')"><i class="fas fa-edit"></i> Редактировать</button><form method="POST" style="margin:0;" onsubmit="return confirm('Удалить проект \'{{ project.title }}\'?');"><input type="hidden" name="action" value="delete_project"><input type="hidden" name="index" value="{{ loop.index0 }}"><button type="submit" class="delete-button"><i class="fas fa-trash-alt"></i> Удалить</button></form></div>
584
+ <div id="edit-project-{{ loop.index0 }}" class="edit-form-container"><form method="POST" enctype="multipart/form-data"><input type="hidden" name="action" value="edit_project"><input type="hidden" name="index" value="{{ loop.index0 }}"><label>Название *:</label><input type="text" name="title" value="{{ project.title }}" required><label>Описание *:</label><textarea name="description" rows="2" required>{{ project.description }}</textarea><label>Заменить фото:</label><input type="file" name="photo" accept="image/*"><button type="submit"><i class="fas fa-save"></i> Сохранить</button></form></div>
585
+ </div>{% endfor %}</div>{% else %}<p>Проектов пока нет.</p>{% endif %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
586
  </div>
587
 
588
  <div class="section">
589
  <h2><i class="fas fa-tags"></i> Категории оборудования</h2>
590
+ <details><summary><i class="fas fa-plus-circle"></i> Добавить категорию</summary><div class="form-content">
591
+ <form method="POST"><input type="hidden" name="action" value="add_category"><label for="add_category_name">Название:</label><input type="text" id="add_category_name" name="category_name" required><button type="submit"><i class="fas fa-plus"></i> Добавить</button></form>
592
+ </div></details>
593
+ {% if categories %}<div class="item-list">{% for category in categories %}<div class="item" style="display: flex; justify-content: space-between; align-items: center;"><span>{{ category }}</span><form method="POST" style="margin: 0;" onsubmit="return confirm('Удалить категорию \'{{ category }}\'?');"><input type="hidden" name="action" value="delete_category"><input type="hidden" name="category_name" value="{{ category }}"><button type="submit" class="delete-button" style="padding: 5px 10px; font-size: 0.8rem; margin: 0;"><i class="fas fa-trash-alt"></i></button></form></div>{% endfor %}</div>{% else %}<p>Категорий пока нет.</p>{% endif %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
594
  </div>
595
 
596
  <div class="section">
597
+ <h2><i class="fas fa-box-open"></i> Оборудование</h2>
598
+ <details><summary><i class="fas fa-plus-circle"></i> Добавить оборудование</summary><div class="form-content">
599
+ <form method="POST" enctype="multipart/form-data"><input type="hidden" name="action" value="add_product"><label for="add_name">Название *:</label><input type="text" id="add_name" name="name" required><label for="add_price">Цена (KGS) *:</label><input type="number" id="add_price" name="price" step="0.01" min="0" required><label for="add_category">Категория:</label><select id="add_category" name="category"><option value="Без категории">Без категории</option>{% for category in categories %}<option value="{{ category }}">{{ category }}</option>{% endfor %}</select><label for="add_photos">Фото:</label><input type="file" id="add_photos" name="photos" accept="image/*" multiple><button type="submit"><i class="fas fa-save"></i> Добавить</button></form>
600
+ </div></details>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
601
  <h3>Список оборудования:</h3>
602
+ {% if products %}<div class="item-list">{% for product in products %}<div class="item">
603
+ <div style="display: flex; gap: 15px; align-items: flex-start;"><div class="photo-preview">{% if product.get('photos') %}<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product['photos'][0] }}" alt="Фото">{% else %}<img src="https://via.placeholder.com/70x70.png?text=N/A">{% endif %}</div><div style="flex-grow: 1;"><h3 style="margin:0 0 5px;">{{ product['name'] }}</h3><p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p><p><strong>Цена:</strong> {{ "%.2f"|format(product['price']) }} KGS</p></div></div>
604
+ <div class="item-actions"><button type="button" onclick="toggleEditForm('edit-form-{{ loop.index0 }}')"><i class="fas fa-edit"></i> Редактировать</button><form method="POST" style="margin:0;" onsubmit="return confirm('Удалить \'{{ product['name'] }}\'?');"><input type="hidden" name="action" value="delete_product"><input type="hidden" name="index" value="{{ loop.index0 }}"><button type="submit" class="delete-button"><i class="fas fa-trash-alt"></i> Удалить</button></form></div>
605
+ <div id="edit-form-{{ loop.index0 }}" class="edit-form-container"><form method="POST" enctype="multipart/form-data"><input type="hidden" name="action" value="edit_product"><input type="hidden" name="index" value="{{ loop.index0 }}"><label>Название *:</label><input type="text" name="name" value="{{ product['name'] }}" required><label>Цена *:</label><input type="number" name="price" step="0.01" min="0" value="{{ product['price'] }}" required><label>Категория:</label><select name="category"><option value="Без категории">Без категории</option>{% for category in categories %}<option value="{{ category }}" {% if product.get('category') == category %}selected{% endif %}>{{ category }}</option>{% endfor %}</select><label>Заменить фото:</label><input type="file" name="photos" accept="image/*" multiple><button type="submit"><i class="fas fa-save"></i> Сохранить</button></form></div>
606
+ </div>{% endfor %}</div>{% else %}<p>Оборудования пока нет.</p>{% endif %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
607
  </div>
608
  </div>
609
  <script>
610
  function toggleEditForm(formId) {
611
+ const form = document.getElementById(formId);
612
+ if (form) form.style.display = form.style.display === 'none' || form.style.display === '' ? 'block' : 'none';
 
 
613
  }
614
  </script>
615
  </body>
 
624
  services=data.get('services', []),
625
  products=data.get('products', []),
626
  categories=sorted(data.get('categories', [])),
627
+ projects=data.get('projects', []),
628
  repo_id=REPO_ID,
629
  contact_phone=CONTACT_PHONE,
630
  whatsapp_phone=WHATSAPP_PHONE,
 
634
  @app.route('/admin', methods=['GET', 'POST'])
635
  def admin():
636
  data = load_data()
 
 
 
637
 
638
  if request.method == 'POST':
639
  action = request.form.get('action')
640
+ logging.info(f"Admin action: {action}")
641
  try:
642
  if action == 'add_category':
643
  category_name = request.form.get('category_name', '').strip()
644
+ if category_name and category_name not in data['categories']:
645
+ data['categories'].append(category_name)
 
646
  save_data(data)
647
  flash(f"Категория '{category_name}' добавлена.", 'success')
648
  else:
649
+ flash("Категория уже существует или пуста.", 'error')
650
 
651
  elif action == 'delete_category':
652
  category_to_delete = request.form.get('category_name')
653
+ if category_to_delete in data['categories']:
654
+ data['categories'].remove(category_to_delete)
655
+ for p in data['products']:
656
+ if p.get('category') == category_to_delete: p['category'] = 'Без категории'
 
 
 
657
  save_data(data)
658
  flash(f"Категория '{category_to_delete}' удалена.", 'success')
659
+
660
+ elif action == 'add_service':
661
+ title, icon, desc = request.form.get('title', ''), request.form.get('icon', ''), request.form.get('description', '')
662
+ if all((title, icon, desc)):
663
+ photo_list = upload_photos_to_hf(request.files.getlist('photo'), title, 'services')
664
+ data['services'].append({'title': title, 'icon': icon, 'description': desc, 'photo': photo_list[0] if photo_list else None})
665
+ save_data(data)
666
+ flash(f"Услуга '{title}' добавлена.", 'success')
667
  else:
668
+ flash("Все поля услуги обязательны.", 'error')
669
 
670
+ elif action == 'edit_service':
671
+ idx = int(request.form.get('index'))
672
+ service = data['services'][idx]
673
+ service['title'], service['icon'], service['description'] = request.form.get('title'), request.form.get('icon'), request.form.get('description')
674
+ if request.files.get('photo'):
675
+ new_photos = upload_photos_to_hf(request.files.getlist('photo'), service['title'], 'services')
676
+ if new_photos:
677
+ delete_photos_from_hf([service.get('photo')], 'services')
678
+ service['photo'] = new_photos[0]
 
 
 
 
 
 
 
 
 
679
  save_data(data)
680
+ flash(f"Услуга '{service['title']}' обновлена.", 'success')
681
 
682
+ elif action == 'delete_service':
683
+ idx = int(request.form.get('index'))
684
+ deleted = data['services'].pop(idx)
685
+ delete_photos_from_hf([deleted.get('photo')], 'services')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
686
  save_data(data)
687
+ flash(f"Услуга '{deleted.get('title')}' удалена.", 'success')
688
 
689
+ elif action == 'add_project':
690
+ title, desc = request.form.get('title', ''), request.form.get('description', '')
691
+ if all((title, desc)) and request.files.get('photo'):
692
+ photo_list = upload_photos_to_hf(request.files.getlist('photo'), title, 'projects')
693
+ data['projects'].append({'title': title, 'description': desc, 'photo': photo_list[0] if photo_list else None})
694
+ save_data(data)
695
+ flash(f"Проект '{title}' добавлен.", 'success')
696
+ else:
697
+ flash("Название, описание и фото проекта обязательны.", 'error')
698
+
699
+ elif action == 'edit_project':
700
+ idx = int(request.form.get('index'))
701
+ project = data['projects'][idx]
702
+ project['title'], project['description'] = request.form.get('title'), request.form.get('description')
703
+ if request.files.get('photo'):
704
+ new_photos = upload_photos_to_hf(request.files.getlist('photo'), project['title'], 'projects')
705
+ if new_photos:
706
+ delete_photos_from_hf([project.get('photo')], 'projects')
707
+ project['photo'] = new_photos[0]
708
  save_data(data)
709
+ flash(f"Проект '{project['title']}' обновлен.", 'success')
710
 
711
+ elif action == 'delete_project':
712
+ idx = int(request.form.get('index'))
713
+ deleted = data['projects'].pop(idx)
714
+ delete_photos_from_hf([deleted.get('photo')], 'projects')
 
 
 
 
 
 
 
 
715
  save_data(data)
716
+ flash(f"Проект '{deleted.get('title')}' удален.", 'success')
717
+
718
+ elif action == 'add_product':
719
+ name, price_str, category = request.form.get('name', ''), request.form.get('price', ''), request.form.get('category')
720
+ if all((name, price_str)):
721
+ photos = upload_photos_to_hf(request.files.getlist('photos'), name, 'photos')
722
+ data['products'].append({'name': name, 'price': float(price_str), 'category': category, 'photos': photos})
723
+ save_data(data)
724
+ flash(f"Оборудование '{name}' добавлено.", 'success')
725
+ else:
726
+ flash("Название и цена обязательны.", 'error')
727
+
728
+ elif action == 'edit_product':
729
+ idx = int(request.form.get('index'))
730
+ product = data['products'][idx]
731
+ product['name'], product['price'], product['category'] = request.form.get('name'), float(request.form.get('price')), request.form.get('category')
732
+ if request.files.getlist('photos'):
733
+ new_photos = upload_photos_to_hf(request.files.getlist('photos'), product['name'], 'photos')
734
+ if new_photos:
735
+ delete_photos_from_hf(product.get('photos', []), 'photos')
736
+ product['photos'] = new_photos
737
  save_data(data)
738
+ flash(f"Оборудование '{product['name']}' обновлено.", 'success')
739
 
740
+ elif action == 'delete_product':
741
+ idx = int(request.form.get('index'))
742
+ deleted = data['products'].pop(idx)
743
+ delete_photos_from_hf(deleted.get('photos', []), 'photos')
 
744
  save_data(data)
745
+ flash(f"Оборудование '{deleted.get('name')}' удалено.", 'success')
746
 
747
  return redirect(url_for('admin'))
748
  except Exception as e:
749
  logging.error(f"Admin action '{action}' failed: {e}", exc_info=True)
750
+ flash(f"Ошибка при выполнении действия: {e}", 'error')
751
  return redirect(url_for('admin'))
752
 
753
  return render_template_string(
 
755
  products=data.get('products', []),
756
  categories=sorted(data.get('categories', [])),
757
  services=data.get('services', []),
758
+ projects=data.get('projects', []),
759
  repo_id=REPO_ID
760
  )
761
 
762
  def upload_photos_to_hf(photo_files, item_name, folder):
763
  if not photo_files or not HF_TOKEN_WRITE:
764
+ if any(f and f.filename for f in photo_files): flash("HF_TOKEN (write) не настроен, фото не загружены.", "warning")
 
765
  return []
766
+ api, uploaded_photos, temp_dir = HfApi(), [], 'uploads_temp'
767
+ os.makedirs(temp_dir, exist_ok=True)
 
 
 
 
768
  for photo in photo_files:
769
  if photo and photo.filename:
770
  try:
771
  ext = os.path.splitext(photo.filename)[1].lower()
772
+ if ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']: continue
 
773
  safe_name = secure_filename(item_name.replace(' ', '_'))[:50]
774
+ filename = f"{safe_name}_{uuid.uuid4().hex[:8]}{ext}"
775
+ temp_path = os.path.join(temp_dir, filename)
776
  photo.save(temp_path)
777
+ api.upload_file(path_or_fileobj=temp_path, path_in_repo=f"{folder}/{filename}", repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE)
778
+ uploaded_photos.append(filename)
 
 
 
 
 
779
  os.remove(temp_path)
780
  except Exception as e:
781
+ logging.error(f"Upload failed for {photo.filename}: {e}", exc_info=True)
 
 
782
  return uploaded_photos
783
 
784
  def delete_photos_from_hf(photo_list, folder):
785
+ if not photo_list or not HF_TOKEN_WRITE: return
786
+ to_delete = [p for p in photo_list if p]
787
+ if not to_delete: return
 
 
 
 
788
  try:
789
+ HfApi().delete_files(repo_id=REPO_ID, paths_in_repo=[f"{folder}/{p}" for p in to_delete], repo_type="dataset", token=HF_TOKEN_WRITE)
790
+ logging.info(f"Deleted photos from HF: {to_delete}")
 
 
 
 
 
791
  except Exception as e:
792
+ logging.error(f"Failed to delete photos from HF: {e}", exc_info=True)
793
  flash("Не удалось удалить старые фото с сервера.", "warning")
794
 
795
  @app.route('/force_upload', methods=['POST'])
796
  def force_upload():
797
+ upload_db_to_hf()
798
+ flash("Данные загружены на сервер.", 'success')
 
 
 
 
799
  return redirect(url_for('admin'))
800
 
801
  @app.route('/force_download', methods=['POST'])
802
  def force_download():
803
+ if download_db_from_hf():
804
+ flash("Данные скачаны с сервера.", 'success')
805
+ else:
806
+ flash("Не удалось скачать данные.", 'error')
 
 
 
 
807
  return redirect(url_for('admin'))
808
 
809
  if __name__ == '__main__':
810
+ logging.info("Application starting...")
811
  download_db_from_hf()
 
812
  if HF_TOKEN_WRITE:
813
+ threading.Thread(target=periodic_backup, daemon=True).start()
 
814
  port = int(os.environ.get('PORT', 7860))
815
  app.run(debug=False, host='0.0.0.0', port=port)