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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +127 -41
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': []}, 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': []}
110
  try:
111
  with open(DATA_FILE, 'r', encoding='utf-8') as file:
112
  data = json.load(file)
@@ -115,21 +115,12 @@ 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
  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,14 +128,8 @@ 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:
@@ -156,8 +141,8 @@ LANDING_TEMPLATE = '''
156
  <head>
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>
@@ -242,7 +227,6 @@ LANDING_TEMPLATE = '''
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;
248
  }
@@ -270,7 +254,7 @@ LANDING_TEMPLATE = '''
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); }
@@ -336,14 +320,45 @@ LANDING_TEMPLATE = '''
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; }
@@ -366,6 +381,7 @@ LANDING_TEMPLATE = '''
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; }
@@ -448,8 +464,8 @@ LANDING_TEMPLATE = '''
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,14 +474,12 @@ LANDING_TEMPLATE = '''
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,10 +489,10 @@ LANDING_TEMPLATE = '''
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>
@@ -539,7 +553,7 @@ LANDING_TEMPLATE = '''
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,7 +593,7 @@ LANDING_TEMPLATE = '''
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';
@@ -610,6 +624,12 @@ LANDING_TEMPLATE = '''
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();
@@ -751,6 +771,38 @@ ADMIN_TEMPLATE = '''
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,6 +895,7 @@ def landing():
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,7 +988,39 @@ def admin():
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,6 +1033,7 @@ def admin():
948
  equipment=data.get('equipment', []),
949
  categories=sorted(data.get('categories', [])),
950
  services=data.get('services', []),
 
951
  repo_id=REPO_ID
952
  )
953
 
 
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
  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
  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
  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:
 
141
  <head>
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>
 
227
  color: var(--text-primary);
228
  text-decoration: none;
229
  font-weight: 500;
 
230
  padding-bottom: 5px;
231
  position: relative;
232
  }
 
254
  background-attachment: fixed;
255
  }
256
  .hero-content { text-align: center; max-width: 850px; margin: 0 auto; }
257
+ .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); }
258
 
259
  .grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 70px; align-items: center; }
260
  .about-img { width: 100%; border-radius: var(--card-border-radius); box-shadow: 0 15px 40px rgba(0,0,0,0.5); }
 
320
  .equipment-card .price { font-size: 1.4rem; font-weight: 700; color: var(--accent-primary); margin: 15px 0; }
321
  .equipment-card .btn { margin-top: 10px; padding: 10px 22px; font-size: 0.95rem; }
322
 
323
+ .projects-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 35px; }
324
+ .project-card {
325
+ position: relative;
326
+ border-radius: var(--card-border-radius);
327
+ overflow: hidden;
328
+ min-height: 420px;
329
+ cursor: pointer;
330
+ box-shadow: 0 10px 30px rgba(0,0,0,0.3);
331
+ transition: box-shadow 0.3s ease;
332
+ }
333
+ .project-card:hover { box-shadow: 0 15px 45px rgba(0,0,0,0.4); }
334
+ .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); }
335
+ .project-overlay {
336
+ position: absolute; bottom: 0; left: 0; right: 0;
337
+ background: linear-gradient(to top, rgba(17, 24, 39, 0.95) 10%, rgba(17, 24, 39, 0) 100%);
338
+ padding: 50px 25px 25px;
339
+ transition: background 0.4s ease;
340
+ }
341
+ .project-card h3 { margin-bottom: 8px; font-size: 1.5rem; color: #fff; }
342
+ .project-card p {
343
+ margin-bottom: 0;
344
+ transition: opacity 0.4s ease, max-height 0.5s ease, transform 0.4s ease;
345
+ opacity: 0; max-height: 0;
346
+ overflow: hidden;
347
+ transform: translateY(10px);
348
+ color: var(--text-secondary);
349
+ }
350
+ .project-card:hover img { transform: scale(1.1); }
351
+ .project-card:hover .project-overlay { background: linear-gradient(to top, rgba(17, 24, 39, 1) 40%, rgba(17, 24, 39, 0) 100%); }
352
+ .project-card:hover p { opacity: 1; max-height: 200px; transform: translateY(0); }
353
+
354
  .contact-content { text-align: center; }
355
  .contact-content > p { max-width: 600px; margin-left: auto; margin-right: auto; }
356
  .contact-info { margin-top: 50px; display: flex; flex-direction: column; align-items: center; gap: 25px; }
357
  .contact-info p { font-size: 1.3rem; margin-bottom: 0; color: var(--text-primary); }
358
  .contact-info a { color: var(--accent-primary); text-decoration: none; font-weight: 600; transition: color 0.3s ease; }
359
  .contact-info a:hover { color: var(--accent-secondary); }
360
+ .contact-info .btn { font-size: 1.1rem; color: #fff !important; } /* Force white color */
361
+ .contact-info .btn i { margin-right: 10px; }
362
 
363
  .footer { text-align: center; padding: 40px 0; background-color: #0c111d; border-top: 1px solid var(--border-color); }
364
  .footer p { color: var(--text-muted); font-size: 0.95rem; }
 
381
  .menu-toggle { display: block; z-index: 1001; }
382
  h1 { font-size: 2.2rem; }
383
  h2 { margin-bottom: 50px; font-size: 1.8rem; }
384
+ .projects-grid { grid-template-columns: 1fr; }
385
  .services-grid { grid-template-columns: 1fr; }
386
  .equipment-grid { grid-template-columns: 1fr; }
387
  .btn { padding: 12px 28px; }
 
464
 
465
  <section id="hero">
466
  <div class="container hero-content">
467
+ <h1>Раина Климат Систем: Профессиональные Климатические Решения</h1>
468
+ <p>Мы предлагаем комплексный подход к созданию идеального микроклимата в ваших помещениях, обеспечивая высочайшее качество услуг и продукции.</p>
469
  <a href="#contact" class="btn">Получить консультацию</a>
470
  </div>
471
  </section>
 
474
  <div class="container">
475
  <h2>О Нашей Компании</h2>
476
  <div class="grid-2">
477
+ <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">
478
  <div>
479
+ <h3>Профессионализм и Экспертиза</h3>
480
+ <p>Наша команда состоит из высококвалифицированных инженеров и техников, обладающих глубокими знаниями и опытом в области HVAC. Мы постоянно совершенствуем свои навыки и внедряем передовые технологии.</p>
481
  <h3>Наша Миссия</h3>
482
+ <p>Мы стремимся создавать оптимальный микроклимат для каждого клиента, обеспечивая комфорт, здоровье и высокую производительность через надежные и энергоэффективные климатические системы.</p>
 
 
483
  </div>
484
  </div>
485
  </div>
 
489
  <div class="container">
490
  <h2>Наши Услуги</h2>
491
  <div class="services-grid">
492
+ <div class="service-card"><i class="fas fa-drafting-compass"></i><h3>Проектирование</h3><p>Точные расчеты, 3D-модели и вся необходимая проектная документация для ваших систем.</p></div>
493
+ <div class="service-card"><i class="fas fa-tools"></i><h3>Монтаж</h3><p>Профессиональная установка всех типов систем вентиляции и кондиционирования, от бытовых до промышленных.</p></div>
494
+ <div class="service-card"><i class="fas fa-headset"></i><h3>Сервис 24/7</h3><p>Плановое техническое обслуживание и оперативный аварийный ремонт для бесперебойной работы ваших систем.</p></div>
495
+ <div class="service-card"><i class="fas fa-sync-alt"></i><h3>Модернизация</h3><p>Повышение энергоэффективности и снижение эксплуатационных расходов за счет оптимизации существующих систем.</p></div>
496
  </div>
497
  </div>
498
  </section>
 
553
  <section id="contact">
554
  <div class="container contact-content">
555
  <h2>Контакты</h2>
556
+ <p>Свяжитесь с нами для профессиональной консультации и подбора оптимального климатического решения для вашего объекта.</p>
557
  <div class="contact-info">
558
  <p><strong>Телефон:</strong> <a href="tel:{{ contact_phone }}">{{ contact_phone }}</a></p>
559
  <a href="https://api.whatsapp.com/send?phone={{ whatsapp_phone }}&text=Здравствуйте, я хотел(а) бы получить консультацию по вашим услугам." target="_blank" class="btn"><i class="fab fa-whatsapp"></i> Написать в WhatsApp</a>
 
593
 
594
  if (type === 'service') allItems = data.services || [];
595
  else if (type === 'equipment') allItems = data.equipment || [];
596
+ else if (type === 'project') allItems = data.projects || [];
597
 
598
  updateModalContent();
599
  document.getElementById('detailsModal').style.display = 'block';
 
624
  <p class="price" style="font-size: 1.8rem; color: var(--accent-primary); margin: 20px 0;">${(item.price || 0).toFixed(2)} KGS</p>
625
  <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>
626
  `;
627
+ } else if (currentType === 'project') {
628
+ content = `
629
+ ${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" alt="Project Image Not Available">'}
630
+ <h3>${item.title || 'Проект'}</h3>
631
+ <p>${item.description || 'Описание отсутствует.'}</p>
632
+ `;
633
  }
634
  modalBody.innerHTML = content;
635
  updateCarouselNav();
 
771
  <form method="POST" action="{{ url_for('force_download') }}" style="display: inline;"><button type="submit" class="button">Скачать с сервера</button></form>
772
  </div>
773
 
774
+ <div class="section">
775
+ <h2><i class="fas fa-star"></i> Реализованные проекты</h2>
776
+ <details><summary>Добавить проект</summary>
777
+ <form method="POST" enctype="multipart/form-data"><input type="hidden" name="action" value="add_project">
778
+ <label>Название*:</label><input type="text" name="title" required>
779
+ <label>Описание*:</label><textarea name="description" rows="3" required></textarea>
780
+ <label>Фото*:</label><input type="file" name="photo" accept="image/*" required>
781
+ <button type="submit">Добавить проект</button>
782
+ </form>
783
+ </details>
784
+ <div class="item-list">
785
+ {% for project in projects %}
786
+ <div class="item">
787
+ <p><strong>{{ project.title }}</strong>: {{ project.description }}</p>
788
+ {% 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 %}
789
+ <div class="item-actions">
790
+ <button onclick="toggleEditForm('edit-project-{{ loop.index0 }}')">Редактировать</button>
791
+ <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>
792
+ </div>
793
+ <div id="edit-project-{{ loop.index0 }}" class="edit-form-container">
794
+ <form method="POST" enctype="multipart/form-data"><input type="hidden" name="action" value="edit_project"><input type="hidden" name="index" value="{{ loop.index0 }}">
795
+ <label>Название*:</label><input type="text" name="title" value="{{ project.title }}" required>
796
+ <label>Описание*:</label><textarea name="description" rows="3" required>{{ project.description }}</textarea>
797
+ <label>Заменить фото:</label><input type="file" name="photo" accept="image/*">
798
+ <button type="submit">Сохранить</button>
799
+ </form>
800
+ </div>
801
+ </div>
802
+ {% endfor %}
803
+ </div>
804
+ </div>
805
+
806
  <div class="section">
807
  <h2><i class="fas fa-concierge-bell"></i> Услуги "под ключ"</h2>
808
  <details><summary>Добавить услугу</summary>
 
895
  services=data.get('services', []),
896
  equipment=data.get('equipment', []),
897
  categories=sorted(data.get('categories', [])),
898
+ projects=data.get('projects', []),
899
  repo_id=REPO_ID,
900
  contact_phone=CONTACT_PHONE,
901
  whatsapp_phone=WHATSAPP_PHONE,
 
988
  item = data['services'].pop(index)
989
  delete_photo_from_hf(item.get('photo'), 'services')
990
  flash(f"Услуга '{item.get('title')}' удалена.", 'success')
991
+
992
+ elif action in ['add_project', 'edit_project']:
993
+ title = request.form.get('title', '').strip()
994
+ item_data = {'title': title, 'description': request.form.get('description')}
995
+ photo = request.files.get('photo')
996
+ if not title:
997
+ flash("Заголовок проекта обязателен.", 'error')
998
+ return redirect(url_for('admin'))
999
+
1000
+ if action == 'add_project':
1001
+ if photo and photo.filename:
1002
+ item_data['photo'] = upload_photo_to_hf(photo, title, 'projects')
1003
+ data['projects'].append(item_data)
1004
+ flash(f"Проект '{title}' добавлен.", 'success')
1005
+ else:
1006
+ flash("Фото обязательно для нового проекта.", 'error')
1007
+ else:
1008
+ index = int(request.form.get('index'))
1009
+ original_item = data['projects'][index]
1010
+ if photo and photo.filename:
1011
+ delete_photo_from_hf(original_item.get('photo'), 'projects')
1012
+ item_data['photo'] = upload_photo_to_hf(photo, title, 'projects')
1013
+ else:
1014
+ item_data['photo'] = original_item.get('photo')
1015
+ data['projects'][index] = item_data
1016
+ flash(f"Проект '{title}' обновлен.", 'success')
1017
+
1018
+ elif action == 'delete_project':
1019
+ index = int(request.form.get('index'))
1020
+ item = data['projects'].pop(index)
1021
+ delete_photo_from_hf(item.get('photo'), 'projects')
1022
+ flash(f"Проект '{item.get('title')}' удален.", 'success')
1023
+
1024
  save_data(data)
1025
  return redirect(url_for('admin'))
1026
  except Exception as e:
 
1033
  equipment=data.get('equipment', []),
1034
  categories=sorted(data.get('categories', [])),
1035
  services=data.get('services', []),
1036
+ projects=data.get('projects', []),
1037
  repo_id=REPO_ID
1038
  )
1039