Kgshop commited on
Commit
b017fac
·
verified ·
1 Parent(s): b90ce12

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +66 -157
app.py CHANGED
@@ -60,7 +60,7 @@ def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWN
60
  if not os.path.exists(file_name):
61
  try:
62
  with open(file_name, 'w', encoding='utf-8') as f:
63
- json.dump({'equipment': [], 'categories': [], 'services': [], 'projects': []}, f)
64
  except Exception as create_e:
65
  logging.error(f"Failed to create empty local file {file_name}: {create_e}")
66
  success = True
@@ -106,7 +106,7 @@ def periodic_backup():
106
  logging.info("Periodic backup finished.")
107
 
108
  def load_data():
109
- default_data = {'equipment': [], 'categories': [], 'services': [], 'projects': []}
110
  try:
111
  with open(DATA_FILE, 'r', encoding='utf-8') as file:
112
  data = json.load(file)
@@ -115,12 +115,21 @@ def load_data():
115
  if 'equipment' not in data: data['equipment'] = []
116
  if 'categories' not in data: data['categories'] = []
117
  if 'services' not in data: data['services'] = []
118
- if 'projects' not in data: data['projects'] = []
119
  return data
120
  except (FileNotFoundError, json.JSONDecodeError, ValueError):
121
  logging.warning(f"Local file {DATA_FILE} not found or corrupt. Attempting download.")
122
  if download_db_from_hf(specific_file=DATA_FILE):
123
- return load_data()
 
 
 
 
 
 
 
 
 
 
124
  return default_data
125
 
126
  def save_data(data):
@@ -128,8 +137,14 @@ def save_data(data):
128
  if not isinstance(data, dict):
129
  logging.error("Attempted to save invalid data structure. Aborting.")
130
  return
 
 
 
 
 
 
131
  with open(DATA_FILE, 'w', encoding='utf-8') as file:
132
- json.dump(data, file, ensure_ascii=False, indent=4)
133
  logging.info(f"Data saved to {DATA_FILE}")
134
  upload_db_to_hf(specific_file=DATA_FILE)
135
  except Exception as e:
@@ -142,21 +157,21 @@ LANDING_TEMPLATE = '''
142
  <meta charset="UTF-8">
143
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
144
  <title>ОсОО "Раина Климат Систем" - Вентиляция и Кондиционирование</title>
145
- <meta name="description" content="Профессиональные услуги по проектированию, монтажу и обслуживанию систем вентиляции и кондиционирования в Кыргызстане. Опыт и экспертиза в климатических решениях.">
146
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
147
  <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap" rel="stylesheet">
148
  <style>
149
  :root {
150
- --bg-dark: #111827; /* Tailwind Gray 900 */
151
- --bg-medium: #1F2937; /* Tailwind Gray 800 */
152
- --bg-light-card: #374151; /* Tailwind Gray 700 */
153
- --text-primary: #F3F4F6; /* Tailwind Gray 100 */
154
- --text-secondary: #D1D5DB; /* Tailwind Gray 300 */
155
- --text-muted: #9CA3AF; /* Tailwind Gray 400 */
156
- --accent-primary: #8B5CF6; /* Violet 500 */
157
- --accent-secondary: #6D28D9; /* Violet 700 */
158
  --accent-glow: rgba(139, 92, 246, 0.25);
159
- --border-color: #374151; /* Tailwind Gray 700 */
160
  --border-hover-color: var(--accent-primary);
161
  --section-padding: clamp(4rem, 10vw, 6rem);
162
  --card-border-radius: 12px;
@@ -216,7 +231,7 @@ LANDING_TEMPLATE = '''
216
  }
217
  .header.scrolled {
218
  padding: 15px 0;
219
- background-color: rgba(17, 24, 39, 0.85); /* bg-dark with opacity */
220
  backdrop-filter: blur(12px);
221
  box-shadow: 0 3px 15px rgba(0,0,0,0.2);
222
  }
@@ -226,7 +241,7 @@ LANDING_TEMPLATE = '''
226
  .nav-links a {
227
  color: var(--text-primary);
228
  text-decoration: none;
229
- font-weight: 500;
230
  transition: color 0.3s ease, transform 0.3s ease;
231
  padding-bottom: 5px;
232
  position: relative;
@@ -252,10 +267,10 @@ LANDING_TEMPLATE = '''
252
  background-image: linear-gradient(rgba(17, 24, 39, 0.75), rgba(17, 24, 39, 1)), url(https://images.unsplash.com/photo-1558221639-2c7158995165?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1740&q=80);
253
  background-size: cover;
254
  background-position: center;
255
- background-attachment: fixed; /* Parallax effect */
256
  }
257
  .hero-content { text-align: center; max-width: 850px; margin: 0 auto; }
258
- .hero-content p { font-size: clamp(1.1rem, 3vw, 1.3rem); margin: 35px 0; max-width: 650px; margin-left: auto; margin-right: auto; color: var(--text-secondary); }
259
 
260
  .grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 70px; align-items: center; }
261
  .about-img { width: 100%; border-radius: var(--card-border-radius); box-shadow: 0 15px 40px rgba(0,0,0,0.5); }
@@ -283,7 +298,6 @@ LANDING_TEMPLATE = '''
283
  .turnkey-content h3 i { transition: color 0.3s ease; }
284
  .turnkey-card:hover .turnkey-content h3 i { color: var(--accent-secondary); }
285
 
286
-
287
  #turnkey { background-color: var(--bg-medium); }
288
  #contact { background-color: var(--bg-medium); }
289
 
@@ -322,45 +336,14 @@ LANDING_TEMPLATE = '''
322
  .equipment-card .price { font-size: 1.4rem; font-weight: 700; color: var(--accent-primary); margin: 15px 0; }
323
  .equipment-card .btn { margin-top: 10px; padding: 10px 22px; font-size: 0.95rem; }
324
 
325
- .projects-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 35px; }
326
- .project-card {
327
- position: relative;
328
- border-radius: var(--card-border-radius);
329
- overflow: hidden;
330
- min-height: 420px;
331
- cursor: pointer;
332
- box-shadow: 0 10px 30px rgba(0,0,0,0.3);
333
- transition: box-shadow 0.3s ease;
334
- }
335
- .project-card:hover { box-shadow: 0 15px 45px rgba(0,0,0,0.4); }
336
- .project-card img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94); }
337
- .project-overlay {
338
- position: absolute; bottom: 0; left: 0; right: 0;
339
- background: linear-gradient(to top, rgba(17, 24, 39, 0.95) 10%, rgba(17, 24, 39, 0) 100%);
340
- padding: 50px 25px 25px;
341
- transition: background 0.4s ease;
342
- }
343
- .project-card h3 { margin-bottom: 8px; font-size: 1.5rem; color: #fff; }
344
- .project-card p {
345
- margin-bottom: 0;
346
- transition: opacity 0.4s ease, max-height 0.5s ease, transform 0.4s ease;
347
- opacity: 0; max-height: 0;
348
- overflow: hidden;
349
- transform: translateY(10px);
350
- color: var(--text-secondary);
351
- }
352
- .project-card:hover img { transform: scale(1.1); }
353
- .project-card:hover .project-overlay { background: linear-gradient(to top, rgba(17, 24, 39, 1) 40%, rgba(17, 24, 39, 0) 100%); }
354
- .project-card:hover p { opacity: 1; max-height: 200px; transform: translateY(0); }
355
-
356
  .contact-content { text-align: center; }
357
  .contact-content > p { max-width: 600px; margin-left: auto; margin-right: auto; }
358
  .contact-info { margin-top: 50px; display: flex; flex-direction: column; align-items: center; gap: 25px; }
359
  .contact-info p { font-size: 1.3rem; margin-bottom: 0; color: var(--text-primary); }
360
  .contact-info a { color: var(--accent-primary); text-decoration: none; font-weight: 600; transition: color 0.3s ease; }
361
  .contact-info a:hover { color: var(--accent-secondary); }
362
- .contact-info .btn { font-size: 1.1rem; }
363
- .contact-info .btn i { margin-right: 10px; }
364
 
365
  .footer { text-align: center; padding: 40px 0; background-color: #0c111d; border-top: 1px solid var(--border-color); }
366
  .footer p { color: var(--text-muted); font-size: 0.95rem; }
@@ -375,15 +358,14 @@ LANDING_TEMPLATE = '''
375
  width: min(80vw, 320px); height: 100vh;
376
  background-color: var(--bg-medium);
377
  flex-direction: column; justify-content: center; align-items: center;
378
- transition: right 0.45s cubic-bezier(0.68, -0.55, 0.27, 1.55);
379
  box-shadow: -8px 0 25px rgba(0,0,0,0.25);
380
- gap: 40px;
381
  }
382
  .nav-links.active { right: 0; }
383
  .menu-toggle { display: block; z-index: 1001; }
384
  h1 { font-size: 2.2rem; }
385
  h2 { margin-bottom: 50px; font-size: 1.8rem; }
386
- .projects-grid { grid-template-columns: 1fr; }
387
  .services-grid { grid-template-columns: 1fr; }
388
  .equipment-grid { grid-template-columns: 1fr; }
389
  .btn { padding: 12px 28px; }
@@ -395,14 +377,14 @@ LANDING_TEMPLATE = '''
395
  z-index: 1001;
396
  left: 0; top: 0; width: 100%; height: 100%;
397
  overflow: auto;
398
- background-color: rgba(17, 24, 39, 0.9);
399
- padding-top: 5vh;
400
  backdrop-filter: blur(5px);
401
  }
402
  .modal-content {
403
  position: relative;
404
  margin: 5% auto;
405
- padding: 30px;
406
  width: 90%;
407
  max-width: 800px;
408
  background-color: var(--bg-medium);
@@ -413,7 +395,7 @@ LANDING_TEMPLATE = '''
413
  }
414
  .modal-content img {
415
  max-width: 100%;
416
- max-height: 65vh;
417
  border-radius: 10px;
418
  margin-bottom: 25px;
419
  object-fit: contain;
@@ -438,12 +420,12 @@ LANDING_TEMPLATE = '''
438
  background-color: var(--accent-primary);
439
  color: white;
440
  border: none;
441
- padding: 12px 18px;
442
  border-radius: var(--button-border-radius);
443
  margin: 0 8px;
444
  cursor: pointer;
445
  transition: all 0.3s ease;
446
- font-size: 1.3rem;
447
  }
448
  .carousel-nav button:hover { background-color: var(--accent-secondary); transform: scale(1.1); }
449
  .carousel-nav button:disabled { background-color: var(--text-muted); cursor: not-allowed; transform: scale(1); }
@@ -466,8 +448,8 @@ LANDING_TEMPLATE = '''
466
 
467
  <section id="hero">
468
  <div class="container hero-content">
469
- <h1>ОсОО "Раина Климат Систем": Экспертиза в Климатических Решениях</h1>
470
- <p>Профессиональный подход к вентиляции и кондиционированию. Создаем оптимальный микроклимат для вашего комфорта и здоровья.</p>
471
  <a href="#contact" class="btn">Получить консультацию</a>
472
  </div>
473
  </section>
@@ -476,14 +458,14 @@ LANDING_TEMPLATE = '''
476
  <div class="container">
477
  <h2>О Нашей Компании</h2>
478
  <div class="grid-2">
479
- <img src="https://images.unsplash.com/photo-1542744173-8e7e53415bb0?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1740&q=80" alt="Команда Раина" class="about-img">
480
  <div>
481
- <h3>Наш Опыт и Профессионализм</h3>
482
- <p>Компания "Раина Климат Систем" специализируется на проектировании, монтаже и обслуживании высококачественных систем вентиляции и кондиционирования.</p>
483
- <h3>Квалифицированные Специалисты</h3>
484
- <p>Наша команда состоит из сертифицированных инженеров и техников, обладающих глубокими знаниями и многолетним опытом в области HVAC.</p>
485
- <h3>Современные Технологии</h3>
486
- <p>Мы используем передовые технологии и оборудование для обеспечения максимальной эффективности, надежности и энергосбережения.</p>
487
  </div>
488
  </div>
489
  </div>
@@ -493,17 +475,17 @@ LANDING_TEMPLATE = '''
493
  <div class="container">
494
  <h2>Наши Услуги</h2>
495
  <div class="services-grid">
496
- <div class="service-card"><i class="fas fa-drafting-compass"></i><h3>Проектирование</h3><p>Разработка детальных проектов вентиляционных и климатических систем с учетом всех ваших требований.</p></div>
497
- <div class="service-card"><i class="fas fa-tools"></i><h3>Монтаж</h3><p>Профессиональная установка и пусконаладка климатического оборудования любого уровня сложности.</p></div>
498
- <div class="service-card"><i class="fas fa-headset"></i><h3>Сервис и Обслуживание</h3><p>Регулярное техническое обслуживание и оперативный ремонт для бесперебойной работы систем.</p></div>
499
- <div class="service-card"><i class="fas fa-sync-alt"></i><h3>Модернизация и Оптимизация</h3><p>Улучшение существующих систем для повышения энергоэффективности и производительности.</p></div>
500
  </div>
501
  </div>
502
  </section>
503
 
504
  <section id="turnkey">
505
  <div class="container">
506
- <h2>Комплексные Решения "Под ключ"</h2>
507
  {% if services %}
508
  <div class="services-grid">
509
  {% for service in services %}
@@ -526,7 +508,7 @@ LANDING_TEMPLATE = '''
526
 
527
  <section id="equipment">
528
  <div class="container">
529
- <h2>Ассортимент Оборудования</h2>
530
  {% if equipment %}
531
  <div class="equipment-filters">
532
  <button class="filter-btn active" data-filter="all">Все</button>
@@ -557,7 +539,7 @@ LANDING_TEMPLATE = '''
557
  <section id="contact">
558
  <div class="container contact-content">
559
  <h2>Контакты</h2>
560
- <p>Свяжитесь с нами ��ля получения профессиональной консультации и подбора оптимального климатического решения для вас.</p>
561
  <div class="contact-info">
562
  <p><strong>Телефон:</strong> <a href="tel:{{ contact_phone }}">{{ contact_phone }}</a></p>
563
  <a href="https://api.whatsapp.com/send?phone={{ whatsapp_phone }}&text=Здравствуйте, я хотел(а) бы получить консультацию по вашим услугам." target="_blank" class="btn"><i class="fab fa-whatsapp"></i> Написать в WhatsApp</a>
@@ -597,7 +579,7 @@ LANDING_TEMPLATE = '''
597
 
598
  if (type === 'service') allItems = data.services || [];
599
  else if (type === 'equipment') allItems = data.equipment || [];
600
- else if (type === 'project') allItems = data.projects || [];
601
 
602
  updateModalContent();
603
  document.getElementById('detailsModal').style.display = 'block';
@@ -611,7 +593,7 @@ LANDING_TEMPLATE = '''
611
 
612
  const item = allItems[currentIndex];
613
  const modalBody = document.getElementById('modal-body');
614
- modalBody.innerHTML = '';
615
  let content = '';
616
 
617
  if (currentType === 'service') {
@@ -622,18 +604,12 @@ LANDING_TEMPLATE = '''
622
  `;
623
  } else if (currentType === 'equipment') {
624
  content = `
625
- ${item.photo ? `<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/equipment/${item.photo}" alt="${item.name}">` : '<img src="https://via.placeholder.com/400x300.png?text=No+Image+Available">'}
626
  <h3>${item.name || 'Оборудование'}</h3>
627
  <p><strong>Категория:</strong> ${item.category || 'Не указана'}</p>
628
  <p class="price" style="font-size: 1.8rem; color: var(--accent-primary); margin: 20px 0;">${(item.price || 0).toFixed(2)} KGS</p>
629
  <a href="https://api.whatsapp.com/send?phone={{ whatsapp_phone }}&text=Здравствуйте, интересует оборудование: ${encodeURIComponent(item.name || '')}" target="_blank" class="btn" style="padding: 14px 30px; font-size: 1.05rem;">Запросить</a>
630
  `;
631
- } else if (currentType === 'project') {
632
- content = `
633
- ${item.photo ? `<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/projects/${item.photo}" alt="${item.title}">` : '<img src="https://via.placeholder.com/400x300.png?text=Project+Image+Not+Available">'}
634
- <h3>${item.title || 'Проект'}</h3>
635
- <p>${item.description || 'Описание отсутствует.'}</p>
636
- `;
637
  }
638
  modalBody.innerHTML = content;
639
  updateCarouselNav();
@@ -681,7 +657,6 @@ LANDING_TEMPLATE = '''
681
  }
682
  });
683
 
684
-
685
  document.addEventListener('DOMContentLoaded', function() {
686
  const header = document.querySelector('.header');
687
  const menuToggle = document.querySelector('.menu-toggle');
@@ -738,7 +713,7 @@ ADMIN_TEMPLATE = '''
738
  body { font-family: 'Poppins', sans-serif; background-color: #f4f7f9; color: #333; padding: 20px; line-height: 1.6; }
739
  .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); }
740
  .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;}
741
- h1, h2, h3 { font-weight: 600; color: #9b59b6; margin-bottom: 15px; }
742
  h1 { font-size: 1.8rem; }
743
  h2 { font-size: 1.5rem; margin-top: 30px; display: flex; align-items: center; gap: 8px; }
744
  .section { margin-bottom: 30px; padding: 20px; background-color: #fafafa; border: 1px solid #e9e9e9; border-radius: 8px; }
@@ -755,7 +730,7 @@ ADMIN_TEMPLATE = '''
755
  .item-actions { margin-top: 15px; display: flex; gap: 10px; flex-wrap: wrap; }
756
  .edit-form-container { margin-top: 15px; padding: 20px; background: #fdf9ff; border: 1px dashed #ddd; border-radius: 6px; display: none; }
757
  details { background-color: #fafafa; border: 1px solid #e9e9e9; border-radius: 8px; margin-bottom: 20px; }
758
- details > summary { cursor: pointer; font-weight: 600; color: #9b59b6; display: block; padding: 15px; position: relative; list-style: none; }
759
  details > summary::after { content: '\\f078'; font-family: 'Font Awesome 6 Free'; font-weight: 900; position: absolute; right: 20px; top: 50%; transform: translateY(-50%); }
760
  details[open] > summary::after { transform: translateY(-50%) rotate(180deg); }
761
  .photo-preview img { max-width: 70px; max-height: 70px; border-radius: 5px; margin: 5px 5px 0 0; object-fit: cover;}
@@ -776,38 +751,6 @@ ADMIN_TEMPLATE = '''
776
  <form method="POST" action="{{ url_for('force_download') }}" style="display: inline;"><button type="submit" class="button">Скачать с сервера</button></form>
777
  </div>
778
 
779
- <div class="section">
780
- <h2><i class="fas fa-star"></i> Реализованные проекты</h2>
781
- <details><summary>Добавить проект</summary>
782
- <form method="POST" enctype="multipart/form-data"><input type="hidden" name="action" value="add_project">
783
- <label>Название*:</label><input type="text" name="title" required>
784
- <label>Описание*:</label><textarea name="description" rows="3" required></textarea>
785
- <label>Фото*:</label><input type="file" name="photo" accept="image/*" required>
786
- <button type="submit">Добавить проект</button>
787
- </form>
788
- </details>
789
- <div class="item-list">
790
- {% for project in projects %}
791
- <div class="item">
792
- <p><strong>{{ project.title }}</strong>: {{ project.description }}</p>
793
- {% if project.photo %}<div class="photo-preview"><img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/projects/{{ project.photo }}" alt="Project Photo"></div>{% endif %}
794
- <div class="item-actions">
795
- <button onclick="toggleEditForm('edit-project-{{ loop.index0 }}')">Редактировать</button>
796
- <form method="POST" style="margin:0;"><input type="hidden" name="action" value="delete_project"><input type="hidden" name="index" value="{{ loop.index0 }}"><button type="submit" class="delete-button">Удалить</button></form>
797
- </div>
798
- <div id="edit-project-{{ loop.index0 }}" class="edit-form-container">
799
- <form method="POST" enctype="multipart/form-data"><input type="hidden" name="action" value="edit_project"><input type="hidden" name="index" value="{{ loop.index0 }}">
800
- <label>Название*:</label><input type="text" name="title" value="{{ project.title }}" required>
801
- <label>Описание*:</label><textarea name="description" rows="3" required>{{ project.description }}</textarea>
802
- <label>Заменить фото:</label><input type="file" name="photo" accept="image/*">
803
- <button type="submit">Сохранить</button>
804
- </form>
805
- </div>
806
- </div>
807
- {% endfor %}
808
- </div>
809
- </div>
810
-
811
  <div class="section">
812
  <h2><i class="fas fa-concierge-bell"></i> Услуги "под ключ"</h2>
813
  <details><summary>Добавить услугу</summary>
@@ -900,7 +843,6 @@ def landing():
900
  services=data.get('services', []),
901
  equipment=data.get('equipment', []),
902
  categories=sorted(data.get('categories', [])),
903
- projects=data.get('projects', []),
904
  repo_id=REPO_ID,
905
  contact_phone=CONTACT_PHONE,
906
  whatsapp_phone=WHATSAPP_PHONE,
@@ -993,39 +935,7 @@ def admin():
993
  item = data['services'].pop(index)
994
  delete_photo_from_hf(item.get('photo'), 'services')
995
  flash(f"Услуга '{item.get('title')}' удалена.", 'success')
996
-
997
- elif action in ['add_project', 'edit_project']:
998
- title = request.form.get('title', '').strip()
999
- item_data = {'title': title, 'description': request.form.get('description')}
1000
- photo = request.files.get('photo')
1001
- if not title:
1002
- flash("Заголовок проекта обязателен.", 'error')
1003
- return redirect(url_for('admin'))
1004
-
1005
- if action == 'add_project':
1006
- if photo and photo.filename:
1007
- item_data['photo'] = upload_photo_to_hf(photo, title, 'projects')
1008
- data['projects'].append(item_data)
1009
- flash(f"Проект '{title}' добавлен.", 'success')
1010
- else:
1011
- flash("Фото обязательно для нового проекта.", 'error')
1012
- else:
1013
- index = int(request.form.get('index'))
1014
- original_item = data['projects'][index]
1015
- if photo and photo.filename:
1016
- delete_photo_from_hf(original_item.get('photo'), 'projects')
1017
- item_data['photo'] = upload_photo_to_hf(photo, title, 'projects')
1018
- else:
1019
- item_data['photo'] = original_item.get('photo')
1020
- data['projects'][index] = item_data
1021
- flash(f"Проект '{title}' обновлен.", 'success')
1022
-
1023
- elif action == 'delete_project':
1024
- index = int(request.form.get('index'))
1025
- item = data['projects'].pop(index)
1026
- delete_photo_from_hf(item.get('photo'), 'projects')
1027
- flash(f"Проект '{item.get('title')}' удален.", 'success')
1028
-
1029
  save_data(data)
1030
  return redirect(url_for('admin'))
1031
  except Exception as e:
@@ -1038,7 +948,6 @@ def admin():
1038
  equipment=data.get('equipment', []),
1039
  categories=sorted(data.get('categories', [])),
1040
  services=data.get('services', []),
1041
- projects=data.get('projects', []),
1042
  repo_id=REPO_ID
1043
  )
1044
 
 
60
  if not os.path.exists(file_name):
61
  try:
62
  with open(file_name, 'w', encoding='utf-8') as f:
63
+ json.dump({'equipment': [], 'categories': [], 'services': []}, f)
64
  except Exception as create_e:
65
  logging.error(f"Failed to create empty local file {file_name}: {create_e}")
66
  success = True
 
106
  logging.info("Periodic backup finished.")
107
 
108
  def load_data():
109
+ default_data = {'equipment': [], 'categories': [], 'services': []}
110
  try:
111
  with open(DATA_FILE, 'r', encoding='utf-8') as file:
112
  data = json.load(file)
 
115
  if 'equipment' not in data: data['equipment'] = []
116
  if 'categories' not in data: data['categories'] = []
117
  if 'services' not in data: data['services'] = []
 
118
  return data
119
  except (FileNotFoundError, json.JSONDecodeError, ValueError):
120
  logging.warning(f"Local file {DATA_FILE} not found or corrupt. Attempting download.")
121
  if download_db_from_hf(specific_file=DATA_FILE):
122
+ # Attempt to load again after successful download
123
+ try:
124
+ with open(DATA_FILE, 'r', encoding='utf-8') as file:
125
+ data = json.load(file)
126
+ if not isinstance(data, dict): data = default_data # Basic safety check
127
+ if 'equipment' not in data: data['equipment'] = []
128
+ if 'categories' not in data: data['categories'] = []
129
+ if 'services' not in data: data['services'] = []
130
+ return data
131
+ except: # If re-load fails, return default
132
+ return default_data
133
  return default_data
134
 
135
  def save_data(data):
 
137
  if not isinstance(data, dict):
138
  logging.error("Attempted to save invalid data structure. Aborting.")
139
  return
140
+ # Ensure only known top-level keys are saved
141
+ cleaned_data = {
142
+ 'equipment': data.get('equipment', []),
143
+ 'categories': data.get('categories', []),
144
+ 'services': data.get('services', [])
145
+ }
146
  with open(DATA_FILE, 'w', encoding='utf-8') as file:
147
+ json.dump(cleaned_data, file, ensure_ascii=False, indent=4)
148
  logging.info(f"Data saved to {DATA_FILE}")
149
  upload_db_to_hf(specific_file=DATA_FILE)
150
  except Exception as e:
 
157
  <meta charset="UTF-8">
158
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
159
  <title>ОсОО "Раина Климат Систем" - Вентиляция и Кондиционирование</title>
160
+ <meta name="description" content="Профессиональные услуги по проектированию, монтажу и обслуживанию систем вентиляции и кондиционирования в Кыргызстане. 15 лет опыта.">
161
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
162
  <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap" rel="stylesheet">
163
  <style>
164
  :root {
165
+ --bg-dark: #111827;
166
+ --bg-medium: #1F2937;
167
+ --bg-light-card: #374151;
168
+ --text-primary: #F3F4F6;
169
+ --text-secondary: #D1D5DB;
170
+ --text-muted: #9CA3AF;
171
+ --accent-primary: #8B5CF6;
172
+ --accent-secondary: #6D28D9;
173
  --accent-glow: rgba(139, 92, 246, 0.25);
174
+ --border-color: #374151;
175
  --border-hover-color: var(--accent-primary);
176
  --section-padding: clamp(4rem, 10vw, 6rem);
177
  --card-border-radius: 12px;
 
231
  }
232
  .header.scrolled {
233
  padding: 15px 0;
234
+ background-color: rgba(17, 24, 39, 0.85);
235
  backdrop-filter: blur(12px);
236
  box-shadow: 0 3px 15px rgba(0,0,0,0.2);
237
  }
 
241
  .nav-links a {
242
  color: var(--text-primary);
243
  text-decoration: none;
244
+ font-weight: 500;
245
  transition: color 0.3s ease, transform 0.3s ease;
246
  padding-bottom: 5px;
247
  position: relative;
 
267
  background-image: linear-gradient(rgba(17, 24, 39, 0.75), rgba(17, 24, 39, 1)), url(https://images.unsplash.com/photo-1558221639-2c7158995165?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1740&q=80);
268
  background-size: cover;
269
  background-position: center;
270
+ background-attachment: fixed;
271
  }
272
  .hero-content { text-align: center; max-width: 850px; margin: 0 auto; }
273
+ .hero-content p { font-size: clamp(1.1rem, 3vw, 1.3rem); margin: 35px 0; max-width: 750px; margin-left: auto; margin-right: auto; color: var(--text-secondary); }
274
 
275
  .grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 70px; align-items: center; }
276
  .about-img { width: 100%; border-radius: var(--card-border-radius); box-shadow: 0 15px 40px rgba(0,0,0,0.5); }
 
298
  .turnkey-content h3 i { transition: color 0.3s ease; }
299
  .turnkey-card:hover .turnkey-content h3 i { color: var(--accent-secondary); }
300
 
 
301
  #turnkey { background-color: var(--bg-medium); }
302
  #contact { background-color: var(--bg-medium); }
303
 
 
336
  .equipment-card .price { font-size: 1.4rem; font-weight: 700; color: var(--accent-primary); margin: 15px 0; }
337
  .equipment-card .btn { margin-top: 10px; padding: 10px 22px; font-size: 0.95rem; }
338
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
339
  .contact-content { text-align: center; }
340
  .contact-content > p { max-width: 600px; margin-left: auto; margin-right: auto; }
341
  .contact-info { margin-top: 50px; display: flex; flex-direction: column; align-items: center; gap: 25px; }
342
  .contact-info p { font-size: 1.3rem; margin-bottom: 0; color: var(--text-primary); }
343
  .contact-info a { color: var(--accent-primary); text-decoration: none; font-weight: 600; transition: color 0.3s ease; }
344
  .contact-info a:hover { color: var(--accent-secondary); }
345
+ .contact-info .btn { font-size: 1.1rem; color: #fff; } /* Ensured white text color for WhatsApp button */
346
+ .contact-info .btn i { margin-right: 10px; color: #fff; } /* Ensured white icon color */
347
 
348
  .footer { text-align: center; padding: 40px 0; background-color: #0c111d; border-top: 1px solid var(--border-color); }
349
  .footer p { color: var(--text-muted); font-size: 0.95rem; }
 
358
  width: min(80vw, 320px); height: 100vh;
359
  background-color: var(--bg-medium);
360
  flex-direction: column; justify-content: center; align-items: center;
361
+ transition: right 0.45s cubic-bezier(0.68, -0.55, 0.27, 1.55);
362
  box-shadow: -8px 0 25px rgba(0,0,0,0.25);
363
+ gap: 40px;
364
  }
365
  .nav-links.active { right: 0; }
366
  .menu-toggle { display: block; z-index: 1001; }
367
  h1 { font-size: 2.2rem; }
368
  h2 { margin-bottom: 50px; font-size: 1.8rem; }
 
369
  .services-grid { grid-template-columns: 1fr; }
370
  .equipment-grid { grid-template-columns: 1fr; }
371
  .btn { padding: 12px 28px; }
 
377
  z-index: 1001;
378
  left: 0; top: 0; width: 100%; height: 100%;
379
  overflow: auto;
380
+ background-color: rgba(17, 24, 39, 0.9);
381
+ padding-top: 5vh;
382
  backdrop-filter: blur(5px);
383
  }
384
  .modal-content {
385
  position: relative;
386
  margin: 5% auto;
387
+ padding: 30px;
388
  width: 90%;
389
  max-width: 800px;
390
  background-color: var(--bg-medium);
 
395
  }
396
  .modal-content img {
397
  max-width: 100%;
398
+ max-height: 65vh;
399
  border-radius: 10px;
400
  margin-bottom: 25px;
401
  object-fit: contain;
 
420
  background-color: var(--accent-primary);
421
  color: white;
422
  border: none;
423
+ padding: 12px 18px;
424
  border-radius: var(--button-border-radius);
425
  margin: 0 8px;
426
  cursor: pointer;
427
  transition: all 0.3s ease;
428
+ font-size: 1.3rem;
429
  }
430
  .carousel-nav button:hover { background-color: var(--accent-secondary); transform: scale(1.1); }
431
  .carousel-nav button:disabled { background-color: var(--text-muted); cursor: not-allowed; transform: scale(1); }
 
448
 
449
  <section id="hero">
450
  <div class="container hero-content">
451
+ <h1>ОсОО "Раина Климат Систем": Ваш Партнер в Вентиляции и Кондиционировании</h1>
452
+ <p>15 лет опыта в создании идеального климата. Мы предлагаем профессиональные решения и гарантируем высокое качество работ, используя самые современные климатические системы для вашего комфорта и здоровья.</p>
453
  <a href="#contact" class="btn">Получить консультацию</a>
454
  </div>
455
  </section>
 
458
  <div class="container">
459
  <h2>О Нашей Компании</h2>
460
  <div class="grid-2">
461
+ <img src="https://images.unsplash.com/photo-1542744173-8e7e53415bb0?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1740&q=80" alt="Команда Раина Климат Систем" class="about-img">
462
  <div>
463
+ <h3>Основание и История</h3>
464
+ <p>Компания "Раина Климат Систем" была основана в 2009 году. За эти годы мы зарекомендовали себя как надежный партнер, стремящийся к инновациям и совершенству в области климатических решений.</p>
465
+ <h3>Наша Миссия</h3>
466
+ <p>Наша миссия создание оптимального микроклимата для наших клиентов, обеспечивающего комфорт, здоровье и высокую производительность.</p>
467
+ <h3>Профессиональная Команда</h3>
468
+ <p>Наша команда состоит из высококвалифицированных инженеров и техников, обладающих глубокими знаниями и опытом в области HVAC.</p>
469
  </div>
470
  </div>
471
  </div>
 
475
  <div class="container">
476
  <h2>Наши Услуги</h2>
477
  <div class="services-grid">
478
+ <div class="service-card"><i class="fas fa-drafting-compass"></i><h3>Проектирование</h3><p>Точные расчеты, 3D-модели и вся необходимая проектная документация.</p></div>
479
+ <div class="service-card"><i class="fas fa-tools"></i><h3>Монтаж</h3><p>Профессиональная установка всех типов систем HVAC, от бытовых до промышленных.</p></div>
480
+ <div class="service-card"><i class="fas fa-headset"></i><h3>Сервис 24/7</h3><p>Плановое обслуживание и оперативный аварийный ремонт в любое время.</p></div>
481
+ <div class="service-card"><i class="fas fa-sync-alt"></i><h3>Модернизация</h3><p>Повышение энергоэффективности и снижение расходов на эксплуатацию.</p></div>
482
  </div>
483
  </div>
484
  </section>
485
 
486
  <section id="turnkey">
487
  <div class="container">
488
+ <h2>Услуги "под ключ"</h2>
489
  {% if services %}
490
  <div class="services-grid">
491
  {% for service in services %}
 
508
 
509
  <section id="equipment">
510
  <div class="container">
511
+ <h2>Наше Оборудование</h2>
512
  {% if equipment %}
513
  <div class="equipment-filters">
514
  <button class="filter-btn active" data-filter="all">Все</button>
 
539
  <section id="contact">
540
  <div class="container contact-content">
541
  <h2>Контакты</h2>
542
+ <p>Готовы стать вашим надежным партнером в создании идеального климата. Свяжитесь с нами для консультации или заказа услуг.</p>
543
  <div class="contact-info">
544
  <p><strong>Телефон:</strong> <a href="tel:{{ contact_phone }}">{{ contact_phone }}</a></p>
545
  <a href="https://api.whatsapp.com/send?phone={{ whatsapp_phone }}&text=Здравствуйте, я хотел(а) бы получить консультацию по вашим услугам." target="_blank" class="btn"><i class="fab fa-whatsapp"></i> Написать в WhatsApp</a>
 
579
 
580
  if (type === 'service') allItems = data.services || [];
581
  else if (type === 'equipment') allItems = data.equipment || [];
582
+ else allItems = [];
583
 
584
  updateModalContent();
585
  document.getElementById('detailsModal').style.display = 'block';
 
593
 
594
  const item = allItems[currentIndex];
595
  const modalBody = document.getElementById('modal-body');
596
+ modalBody.innerHTML = '';
597
  let content = '';
598
 
599
  if (currentType === 'service') {
 
604
  `;
605
  } else if (currentType === 'equipment') {
606
  content = `
607
+ ${item.photo ? `<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/equipment/${item.photo}" alt="${item.name}">` : '<img src="https://via.placeholder.com/400x300.png?text=No+Image" alt="No Image Available">'}
608
  <h3>${item.name || 'Оборудование'}</h3>
609
  <p><strong>Категория:</strong> ${item.category || 'Не указана'}</p>
610
  <p class="price" style="font-size: 1.8rem; color: var(--accent-primary); margin: 20px 0;">${(item.price || 0).toFixed(2)} KGS</p>
611
  <a href="https://api.whatsapp.com/send?phone={{ whatsapp_phone }}&text=Здравствуйте, интересует оборудование: ${encodeURIComponent(item.name || '')}" target="_blank" class="btn" style="padding: 14px 30px; font-size: 1.05rem;">Запросить</a>
612
  `;
 
 
 
 
 
 
613
  }
614
  modalBody.innerHTML = content;
615
  updateCarouselNav();
 
657
  }
658
  });
659
 
 
660
  document.addEventListener('DOMContentLoaded', function() {
661
  const header = document.querySelector('.header');
662
  const menuToggle = document.querySelector('.menu-toggle');
 
713
  body { font-family: 'Poppins', sans-serif; background-color: #f4f7f9; color: #333; padding: 20px; line-height: 1.6; }
714
  .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); }
715
  .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;}
716
+ h1, h2, h3 { font-weight: 600; color: #6a0dad; margin-bottom: 15px; }
717
  h1 { font-size: 1.8rem; }
718
  h2 { font-size: 1.5rem; margin-top: 30px; display: flex; align-items: center; gap: 8px; }
719
  .section { margin-bottom: 30px; padding: 20px; background-color: #fafafa; border: 1px solid #e9e9e9; border-radius: 8px; }
 
730
  .item-actions { margin-top: 15px; display: flex; gap: 10px; flex-wrap: wrap; }
731
  .edit-form-container { margin-top: 15px; padding: 20px; background: #fdf9ff; border: 1px dashed #ddd; border-radius: 6px; display: none; }
732
  details { background-color: #fafafa; border: 1px solid #e9e9e9; border-radius: 8px; margin-bottom: 20px; }
733
+ details > summary { cursor: pointer; font-weight: 600; color: #8e44ad; display: block; padding: 15px; position: relative; list-style: none; }
734
  details > summary::after { content: '\\f078'; font-family: 'Font Awesome 6 Free'; font-weight: 900; position: absolute; right: 20px; top: 50%; transform: translateY(-50%); }
735
  details[open] > summary::after { transform: translateY(-50%) rotate(180deg); }
736
  .photo-preview img { max-width: 70px; max-height: 70px; border-radius: 5px; margin: 5px 5px 0 0; object-fit: cover;}
 
751
  <form method="POST" action="{{ url_for('force_download') }}" style="display: inline;"><button type="submit" class="button">Скачать с сервера</button></form>
752
  </div>
753
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
754
  <div class="section">
755
  <h2><i class="fas fa-concierge-bell"></i> Услуги "под ключ"</h2>
756
  <details><summary>Добавить услугу</summary>
 
843
  services=data.get('services', []),
844
  equipment=data.get('equipment', []),
845
  categories=sorted(data.get('categories', [])),
 
846
  repo_id=REPO_ID,
847
  contact_phone=CONTACT_PHONE,
848
  whatsapp_phone=WHATSAPP_PHONE,
 
935
  item = data['services'].pop(index)
936
  delete_photo_from_hf(item.get('photo'), 'services')
937
  flash(f"Услуга '{item.get('title')}' удалена.", 'success')
938
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
939
  save_data(data)
940
  return redirect(url_for('admin'))
941
  except Exception as e:
 
948
  equipment=data.get('equipment', []),
949
  categories=sorted(data.get('categories', [])),
950
  services=data.get('services', []),
 
951
  repo_id=REPO_ID
952
  )
953