Kgshop commited on
Commit
8c6028c
·
verified ·
1 Parent(s): 04e2bd4

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +359 -254
app.py CHANGED
@@ -144,212 +144,309 @@ LANDING_TEMPLATE = '''
144
  <title>ОсОО "Раина" - Вентиляция и Кондиционирование</title>
145
  <meta name="description" content="Профессиональные услуги по проектированию, монтажу и обслуживанию систем вентиляции и кондиционирования в Кыргызстане. 15 лет опыта, более 1000 проектов.">
146
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
147
- <link rel="preconnect" href="https://fonts.googleapis.com">
148
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
149
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&display=swap" rel="stylesheet">
150
  <style>
151
  :root {
152
- --bg-dark: #0f172a;
153
- --bg-card: rgba(30, 41, 59, 0.5);
154
- --bg-card-hover: rgba(51, 65, 85, 0.7);
155
- --border-color: rgba(255, 255, 255, 0.1);
156
- --text-light: #f8fafc;
157
- --text-normal: #cbd5e1;
158
- --text-muted: #94a3b8;
159
- --primary-gradient: linear-gradient(90deg, #4f46e5, #a855f7, #ec4899);
160
- --primary-color: #a855f7;
161
- --accent-glow: rgba(168, 85, 247, 0.15);
 
 
 
 
162
  }
163
  * { margin: 0; padding: 0; box-sizing: border-box; scroll-behavior: smooth; }
164
- body {
165
- font-family: 'Inter', sans-serif;
166
- background-color: var(--bg-dark);
167
- color: var(--text-normal);
168
- line-height: 1.7;
169
- font-size: 16px;
170
  -webkit-font-smoothing: antialiased;
171
  -moz-osx-font-smoothing: grayscale;
172
  }
173
  .container { max-width: 1200px; margin: 0 auto; padding: 0 20px; }
174
- section { padding: clamp(4rem, 10vw, 6rem) 0; overflow: hidden; position: relative; }
175
- h1, h2, h3 { font-weight: 700; color: var(--text-light); line-height: 1.3; }
176
- h1 { font-size: clamp(2.5rem, 6vw, 4rem); font-weight: 800; }
177
- h2 { font-size: clamp(2rem, 5vw, 3rem); text-align: center; margin-bottom: 60px; position: relative; }
178
- h2::after {
179
- content: ''; display: block; width: 100px; height: 4px;
180
- background: var(--primary-gradient);
181
- margin: 15px auto 0; border-radius: 2px;
182
  }
183
- h3 { font-size: clamp(1.2rem, 3vw, 1.5rem); margin-bottom: 15px; }
184
- p { margin-bottom: 1rem; color: var(--text-muted); }
185
- .btn {
186
- display: inline-block; padding: 14px 32px;
187
- background: var(--primary-gradient); background-size: 200% 100%;
188
- color: #fff; border-radius: 50px; text-decoration: none;
189
- font-weight: 600; transition: all 0.4s ease;
190
- border: none; box-shadow: 0 5px 20px rgba(168, 85, 247, 0.2);
191
  }
192
- .btn:hover {
193
- background-position: right center;
194
- transform: translateY(-3px) scale(1.02);
195
- box-shadow: 0 8px 25px rgba(168, 85, 247, 0.3);
 
 
 
 
 
 
 
 
 
 
196
  }
197
- .header {
198
- position: fixed; top: 0; left: 0; width: 100%; z-index: 1000;
199
- padding: 15px 0; transition: all 0.3s ease;
 
200
  }
201
- .header.scrolled {
202
- background-color: rgba(15, 23, 42, 0.8);
203
- backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
204
- border-bottom: 1px solid var(--border-color);
205
- padding: 10px 0;
 
 
 
 
 
 
206
  }
207
  .navbar { display: flex; justify-content: space-between; align-items: center; }
208
- .logo { font-size: clamp(1.5rem, 4vw, 1.8rem); font-weight: 800; color: #fff; text-decoration: none; }
209
  .nav-links { display: flex; gap: 35px; list-style: none; }
210
- .nav-links a { color: var(--text-normal); text-decoration: none; font-weight: 600; transition: color 0.3s ease; position: relative; padding-bottom: 5px; }
 
 
 
 
 
 
 
211
  .nav-links a::after {
212
- content: ''; position: absolute; bottom: 0; left: 0;
213
- width: 0; height: 2px; background: var(--primary-gradient);
 
 
 
 
 
 
214
  transition: width 0.3s ease;
215
  }
216
- .nav-links a:hover { color: var(--text-light); }
217
  .nav-links a:hover::after { width: 100%; }
218
- .menu-toggle { display: none; font-size: 1.5rem; cursor: pointer; border: none; background: none; color: white; }
219
- #hero {
220
- min-height: 100vh; display: flex; align-items: center;
221
- background-image: linear-gradient(rgba(15, 23, 42, 0.7), var(--bg-dark)), 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);
222
- background-size: cover; background-position: center; position: relative;
223
- }
224
- #hero::before {
225
- content: ''; position: absolute; top: 0; left: 50%;
226
- transform: translateX(-50%); width: 70%; height: 100%;
227
- background: radial-gradient(circle, rgba(79, 70, 229, 0.15) 0%, rgba(79, 70, 229, 0) 70%);
228
- pointer-events: none; z-index: 0;
229
- }
230
- .hero-content { text-align: center; max-width: 800px; margin: 0 auto; position: relative; z-index: 1; }
231
- .hero-content h1 {
232
- background: var(--primary-gradient);
233
- -webkit-background-clip: text; -webkit-text-fill-color: transparent;
234
- background-clip: text; text-fill-color: transparent;
235
  }
236
- .hero-content p { font-size: clamp(1rem, 2.5vw, 1.25rem); margin: 30px 0; max-width: 650px; margin-left: auto; margin-right: auto; color: var(--text-normal); }
237
- .grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 60px; align-items: center; }
238
- .about-img { width: 100%; border-radius: 16px; box-shadow: 0 10px 40px rgba(0,0,0,0.3); }
239
- .services-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 30px; }
240
- .interactive-card {
241
- background: var(--bg-card); padding: 35px 30px; border-radius: 16px;
242
- border: 1px solid var(--border-color);
243
- transition: all 0.3s ease-out; position: relative; overflow: hidden;
 
 
 
 
 
 
244
  }
245
- .interactive-card:hover {
246
- transform: translateY(-8px); background: var(--bg-card-hover);
247
- border-color: rgba(168, 85, 247, 0.5); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
 
248
  }
249
- .interactive-card::before {
250
- content: ""; position: absolute; top: 0; left: 0; width: 100%; height: 100%;
251
- background: radial-gradient(400px circle at var(--mouse-x) var(--mouse-y), var(--accent-glow), transparent 40%);
252
- opacity: 0; transition: opacity 0.5s; pointer-events: none;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
  }
254
- .interactive-card:hover::before { opacity: 1; }
255
- .service-card i { font-size: 2.5rem; color: var(--primary-color); margin-bottom: 20px; display: block; }
256
- #turnkey { background-color: #0b1222; }
257
- .turnkey-card { padding: 0; display: flex; flex-direction: column; cursor: pointer;}
258
- .turnkey-img { width: 100%; height: 200px; object-fit: cover; border-radius: 16px 16px 0 0; }
259
- .turnkey-content { padding: 30px; flex-grow: 1; }
260
- .equipment-filters { display: flex; justify-content: center; flex-wrap: wrap; gap: 15px; margin-bottom: 50px; }
261
- .filter-btn {
262
- padding: 10px 25px; border: 1px solid var(--border-color); background-color: transparent;
263
- color: var(--text-normal); border-radius: 50px; cursor: pointer; transition: all 0.3s; font-weight: 600;
 
 
264
  }
265
- .filter-btn.active, .filter-btn:hover {
266
- background: var(--primary-gradient); color: #fff; border-color: transparent;
 
 
267
  }
268
- .equipment-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 30px; }
269
- .equipment-card { text-align: center; padding: 25px; cursor: pointer; }
270
- .equipment-card img { width: 100%; height: 180px; object-fit: contain; margin-bottom: 20px; transition: transform 0.3s ease; }
271
  .equipment-card:hover img { transform: scale(1.05); }
272
- .equipment-card h3 { font-size: 1.2rem; }
273
- .equipment-card .price { font-size: 1.4rem; font-weight: 700; color: var(--text-light); margin: 10px 0; }
274
- .equipment-card .btn { padding: 10px 22px; font-size: 0.9rem; }
275
- .projects-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 30px; }
276
- .project-card {
277
- position: relative; border-radius: 16px; overflow: hidden;
278
- min-height: 450px; cursor: pointer;
279
- box-shadow: 0 5px 15px rgba(0,0,0,0.2);
280
- transition: all 0.4s ease;
 
 
 
 
281
  }
282
- .project-card:hover { transform: translateY(-8px); box-shadow: 0 15px 30px rgba(0,0,0,0.3); }
283
- .project-card img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform 0.4s ease; }
284
- .project-overlay {
285
- position: absolute; bottom: 0; left: 0; right: 0;
286
- background: linear-gradient(to top, rgba(15, 23, 42, 1) 10%, rgba(15, 23, 42, 0) 100%);
287
- padding: 50px 25px 25px;
 
288
  }
289
- .project-card h3 { margin-bottom: 5px; font-size: 1.4rem; }
290
- .project-card p {
291
- margin-bottom: 0; transition: all 0.4s ease; opacity: 0;
292
- max-height: 0; overflow: hidden; transform: translateY(10px);
 
 
 
 
293
  }
294
- .project-card:hover img { transform: scale(1.05); }
 
295
  .project-card:hover p { opacity: 1; max-height: 200px; transform: translateY(0); }
296
- #contact { background-color: #0b1222; }
297
  .contact-content { text-align: center; }
298
- .contact-info { margin-top: 40px; display: flex; flex-direction: column; align-items: center; gap: 25px; }
299
- .contact-info p { font-size: 1.2rem; margin-bottom: 0; }
300
- .contact-info a { color: var(--primary-color); text-decoration: none; font-weight: 600; font-size: 1.2rem; transition: color 0.3s; }
301
- .contact-info a:hover { color: var(--text-light); }
302
- .footer { text-align: center; padding: 40px 0; background-color: #020617; }
 
 
 
 
 
 
303
  @media (max-width: 992px) {
304
  .grid-2 { grid-template-columns: 1fr; text-align: center; }
305
- .grid-2 > div { order: 1; }
306
- .about-img { margin-bottom: 40px; max-width: 500px; margin-left: auto; margin-right: auto; order: 2; }
307
  }
308
  @media (max-width: 768px) {
309
- body { font-size: 15px; }
310
- .nav-links {
311
- position: fixed; top: 0; right: -100%; width: min(80vw, 320px); height: 100vh;
312
- background-color: var(--bg-dark); flex-direction: column; justify-content: center;
313
- align-items: center; transition: right 0.4s ease-in-out;
314
- box-shadow: -5px 0 15px rgba(0,0,0,0.2); z-index: 1000;
 
 
315
  }
316
  .nav-links.active { right: 0; }
317
  .menu-toggle { display: block; z-index: 1001; }
318
- h2 { margin-bottom: 40px; }
 
319
  .projects-grid { grid-template-columns: 1fr; }
 
 
 
320
  }
 
321
  .modal {
322
- display: none; position: fixed; z-index: 1001; left: 0; top: 0;
323
- width: 100%; height: 100%; overflow: auto;
324
- background-color: rgba(15, 23, 42, 0.9); backdrop-filter: blur(8px);
325
- padding-top: 60px;
 
 
 
 
326
  }
327
  .modal-content {
328
- position: relative; margin: 5% auto; padding: 30px;
329
- width: 90%; max-width: 800px; background-color: #0b1222;
330
- border-radius: 16px; text-align: center; border: 1px solid var(--border-color);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
331
  }
332
- .modal-content img { max-width: 100%; max-height: 70vh; border-radius: 10px; margin-bottom: 25px; }
333
- .modal-content h3 { color: var(--primary-color); margin-bottom: 10px; }
334
- .modal-content p { color: var(--text-normal); font-size: 1.1rem; }
335
  .close-button {
336
- position: absolute; top: 15px; right: 25px; font-size: 2.5rem;
337
- font-weight: bold; color: #fff; cursor: pointer;
338
- background: none; border: none; transition: color 0.3s, transform 0.3s;
 
 
 
 
 
 
 
339
  }
340
- .close-button:hover, .close-button:focus { color: var(--primary-color); transform: rotate(90deg); }
341
- .carousel-nav { margin-top: 20px; }
342
  .carousel-nav button {
343
- background-color: var(--primary-color); color: white; border: none;
344
- padding: 10px 15px; border-radius: 50px; margin: 0 8px;
345
- cursor: pointer; transition: all 0.3s ease; font-size: 1.2rem;
346
- }
347
- .carousel-nav button:hover { background-color: #4f46e5; transform: scale(1.1); }
348
- .animate-on-scroll {
349
- opacity: 0; transform: translateY(40px);
350
- transition: opacity 0.7s ease-out, transform 0.7s ease-out;
 
351
  }
352
- .animate-on-scroll.is-visible { opacity: 1; transform: translateY(0); }
 
353
  </style>
354
  </head>
355
  <body>
@@ -377,7 +474,7 @@ LANDING_TEMPLATE = '''
377
  </section>
378
 
379
  <section id="about">
380
- <div class="container animate-on-scroll">
381
  <h2>О Нашей Компании</h2>
382
  <div class="grid-2">
383
  <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">
@@ -394,42 +491,42 @@ LANDING_TEMPLATE = '''
394
  </section>
395
 
396
  <section id="services">
397
- <div class="container animate-on-scroll">
398
  <h2>Наши Услуги</h2>
399
  <div class="services-grid">
400
- <div class="service-card interactive-card"><i class="fas fa-drafting-compass"></i><h3>Проектирование</h3><p>Точные расчеты, 3D-модели и вся необходимая проектная документация.</p></div>
401
- <div class="service-card interactive-card"><i class="fas fa-tools"></i><h3>Монтаж</h3><p>Профессиональная установка всех типов систем HVAC, от бытовых до промышленных.</p></div>
402
- <div class="service-card interactive-card"><i class="fas fa-headset"></i><h3>Сервис 24/7</h3><p>Плановое обслуживание и оперативный аварийный ремонт в любое время.</p></div>
403
- <div class="service-card interactive-card"><i class="fas fa-sync-alt"></i><h3>Модернизация</h3><p>Повышение энергоэффективности и снижение расходов на эксплуатацию.</p></div>
404
  </div>
405
  </div>
406
  </section>
407
 
408
  <section id="turnkey">
409
- <div class="container animate-on-scroll">
410
  <h2>Услуги "под ключ"</h2>
411
  {% if services %}
412
  <div class="services-grid">
413
  {% for service in services %}
414
- <div class="turnkey-card interactive-card" onclick="showDetailsModal('service', {{ loop.index0 }})">
415
  {% if service.photo %}
416
  <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/services/{{ service.photo }}" alt="{{ service.title }}" class="turnkey-img">
417
  {% endif %}
418
  <div class="turnkey-content">
419
- <h3><i class="{{ service.icon }} fa-fw" style="margin-right: 8px; color: var(--primary-color);"></i>{{ service.title }}</h3>
420
  <p>{{ service.description }}</p>
421
  </div>
422
  </div>
423
  {% endfor %}
424
  </div>
425
  {% else %}
426
- <p style="text-align: center;">Информация об услугах "под ключ" скоро появится на сайте.</p>
427
  {% endif %}
428
  </div>
429
  </section>
430
 
431
  <section id="equipment">
432
- <div class="container animate-on-scroll">
433
  <h2>Наше Оборудование</h2>
434
  {% if equipment %}
435
  <div class="equipment-filters">
@@ -440,26 +537,26 @@ LANDING_TEMPLATE = '''
440
  </div>
441
  <div class="equipment-grid">
442
  {% for item in equipment %}
443
- <div class="equipment-card interactive-card" data-category="{{ item.get('category', 'all') }}" onclick="showDetailsModal('equipment', {{ loop.index0 }})">
444
  {% if item.photo %}
445
  <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/equipment/{{ item.photo }}" alt="{{ item.name }}">
446
  {% else %}
447
- <img src="https://via.placeholder.com/250x180.png?text=No+Image" alt="No Image">
448
  {% endif %}
449
  <h3>{{ item.name }}</h3>
450
  <p class="price">{{ "%.2f"|format(item.price) }} KGS</p>
451
- <a href="https://api.whatsapp.com/send?phone={{ whatsapp_phone }}&text=Здравствуйте, интересует оборудование: {{ item.name }}" target="_blank" class="btn" onclick="event.stopPropagation()">Запросить</a>
452
  </div>
453
  {% endfor %}
454
  </div>
455
  {% else %}
456
- <p style="text-align: center;">Каталог оборудования скоро будет доступен.</p>
457
  {% endif %}
458
  </div>
459
  </section>
460
 
461
  <section id="projects">
462
- <div class="container animate-on-scroll">
463
  <h2>Реализованные Проекты</h2>
464
  {% if projects %}
465
  <div class="projects-grid">
@@ -474,20 +571,20 @@ LANDING_TEMPLATE = '''
474
  {% endfor %}
475
  </div>
476
  {% else %}
477
- <p style="text-align: center;">Информация о реализованных проектах скоро появится на сайте.</p>
478
  {% endif %}
479
  </div>
480
  </section>
481
 
482
  <section id="contact">
483
- <div class="container contact-content animate-on-scroll">
484
  <h2>Контакты</h2>
485
- <p>Готовы стать вашим надежным партнером в создании идеального климата.</p>
486
  <div class="contact-info">
487
- <p><strong>Свяжитесь с нами:</strong> <a href="tel:{{ contact_phone }}">{{ contact_phone }}</a></p>
488
- <a href="https://api.whatsapp.com/send?phone={{ whatsapp_phone }}&text=Здравствуйте, я хотел(а) бы получить консультацию по вашим услугам." target="_blank" class="btn"><i class="fab fa-whatsapp" style="margin-right: 8px;"></i> Написать в WhatsApp</a>
489
  </div>
490
- <div style="margin-top: 40px; font-size: 0.9rem; color: var(--text-muted);">
491
  <p><strong>Реквизиты:</strong> ОсОО «Раина», ИНН: 00812202110194, ОКПО: 31290279</p>
492
  </div>
493
  </div>
@@ -499,11 +596,11 @@ LANDING_TEMPLATE = '''
499
 
500
  <div id="detailsModal" class="modal">
501
  <div class="modal-content">
502
- <button class="close-button" onclick="closeDetailsModal()">×</button>
503
  <div id="modal-body"></div>
504
  <div class="carousel-nav">
505
- <button id="prevBtn" onclick="changeModalItem(-1)">❮</button>
506
- <button id="nextBtn" onclick="changeModalItem(1)">❯</button>
507
  </div>
508
  </div>
509
  </div>
@@ -515,64 +612,51 @@ LANDING_TEMPLATE = '''
515
  let allItems = [];
516
 
517
  function showDetailsModal(type, index) {
518
- event.stopPropagation();
519
  const data = {{ data | tojson }};
520
  currentData = data;
521
  currentType = type;
522
-
523
- if (type === 'service') allItems = data.services;
524
- else if (type === 'equipment') {
525
- const activeFilter = document.querySelector('.filter-btn.active').dataset.filter;
526
- allItems = (activeFilter === 'all') ? data.equipment : data.equipment.filter(i => i.category === activeFilter);
527
- const originalItem = data.equipment[index];
528
- currentIndex = allItems.findIndex(i => i.name === originalItem.name && i.price === originalItem.price);
529
- if (currentIndex === -1) {
530
- allItems = data.equipment;
531
- currentIndex = index;
532
- }
533
- }
534
- else if (type === 'project') allItems = data.projects;
535
- else {
536
- allItems = [];
537
- currentIndex = -1;
538
- }
539
-
540
- if (allItems.length > 0 && type !== 'equipment') {
541
- currentIndex = index;
542
- }
543
 
 
 
 
 
544
  updateModalContent();
545
  document.getElementById('detailsModal').style.display = 'block';
546
  document.body.style.overflow = 'hidden';
547
  }
548
 
549
  function updateModalContent() {
550
- if (!allItems || currentIndex < 0 || currentIndex >= allItems.length) return;
 
 
 
 
551
 
552
  const item = allItems[currentIndex];
553
  const modalBody = document.getElementById('modal-body');
554
- modalBody.innerHTML = '';
555
  let content = '';
556
 
557
  if (currentType === 'service') {
558
  content = `
559
  ${item.photo ? `<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/services/${item.photo}" alt="${item.title}">` : ''}
560
- <h3><i class="${item.icon} fa-fw" style="margin-right: 8px;"></i>${item.title}</h3>
561
- <p>${item.description}</p>
562
  `;
563
  } else if (currentType === 'equipment') {
564
  content = `
565
- ${item.photo ? `<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/equipment/${item.photo}" alt="${item.name}">` : ''}
566
- <h3>${item.name}</h3>
567
  <p><strong>Категория:</strong> ${item.category || 'Не указана'}</p>
568
- <p class="price" style="font-size: 1.5rem; color: var(--primary-color);">${item.price.toFixed(2)} KGS</p>
569
- <a href="https://api.whatsapp.com/send?phone={{ whatsapp_phone }}&text=Здравствуйте, интересует оборудование: ${encodeURIComponent(item.name)}" target="_blank" class="btn" style="padding: 12px 28px; font-size: 1rem;">Запросить</a>
570
  `;
571
  } else if (currentType === 'project') {
572
  content = `
573
- <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/projects/${item.photo}" alt="${item.title}">
574
- <h3>${item.title}</h3>
575
- <p>${item.description}</p>
576
  `;
577
  }
578
  modalBody.innerHTML = content;
@@ -580,6 +664,7 @@ LANDING_TEMPLATE = '''
580
  }
581
 
582
  function changeModalItem(direction) {
 
583
  let newIndex = currentIndex + direction;
584
  if (newIndex < 0) newIndex = allItems.length - 1;
585
  if (newIndex >= allItems.length) newIndex = 0;
@@ -590,23 +675,40 @@ LANDING_TEMPLATE = '''
590
  function updateCarouselNav() {
591
  const prevBtn = document.getElementById('prevBtn');
592
  const nextBtn = document.getElementById('nextBtn');
593
- if (allItems.length <= 1) {
594
  prevBtn.style.display = 'none';
595
  nextBtn.style.display = 'none';
596
  } else {
597
  prevBtn.style.display = 'inline-block';
598
  nextBtn.style.display = 'inline-block';
 
 
599
  }
600
  }
601
 
602
  function closeDetailsModal() {
603
  document.getElementById('detailsModal').style.display = 'none';
604
  document.body.style.overflow = '';
605
- currentData = null;
606
- currentType = null;
607
- currentIndex = -1;
608
- allItems = [];
609
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
610
 
611
  document.addEventListener('DOMContentLoaded', function() {
612
  const header = document.querySelector('.header');
@@ -619,11 +721,13 @@ LANDING_TEMPLATE = '''
619
 
620
  menuToggle.addEventListener('click', () => {
621
  navLinks.classList.toggle('active');
 
622
  });
623
 
624
  document.querySelectorAll('.nav-links a').forEach(link => {
625
  link.addEventListener('click', () => {
626
  navLinks.classList.remove('active');
 
627
  });
628
  });
629
 
@@ -631,36 +735,18 @@ LANDING_TEMPLATE = '''
631
  if (filterContainer) {
632
  filterContainer.addEventListener('click', (e) => {
633
  if (!e.target.matches('.filter-btn')) return;
634
- filterContainer.querySelector('.active').classList.remove('active');
 
 
 
635
  e.target.classList.add('active');
636
  const filter = e.target.dataset.filter;
 
637
  document.querySelectorAll('.equipment-card').forEach(card => {
638
- card.style.display = (filter === 'all' || card.dataset.category === filter) ? 'flex' : 'none';
639
  });
640
  });
641
  }
642
-
643
- document.querySelectorAll('.interactive-card').forEach(card => {
644
- card.addEventListener('mousemove', e => {
645
- const rect = card.getBoundingClientRect();
646
- const x = e.clientX - rect.left;
647
- const y = e.clientY - rect.top;
648
- card.style.setProperty('--mouse-x', `${x}px`);
649
- card.style.setProperty('--mouse-y', `${y}px`);
650
- });
651
- });
652
-
653
- const scrollElements = document.querySelectorAll(".animate-on-scroll");
654
- const elementInView = (el, dividend = 1) => {
655
- const elementTop = el.getBoundingClientRect().top;
656
- return (elementTop <= (window.innerHeight || document.documentElement.clientHeight) / dividend);
657
- };
658
- const displayScrollElement = (element) => { element.classList.add("is-visible"); };
659
- const handleScrollAnimation = () => {
660
- scrollElements.forEach((el) => { if (elementInView(el, 1.15)) { displayScrollElement(el); } })
661
- }
662
- window.addEventListener("scroll", () => { handleScrollAnimation(); });
663
- handleScrollAnimation();
664
  });
665
  </script>
666
  </body>
@@ -847,7 +933,7 @@ def landing():
847
  contact_phone=CONTACT_PHONE,
848
  whatsapp_phone=WHATSAPP_PHONE,
849
  now=datetime.utcnow(),
850
- data=data
851
  )
852
 
853
  @app.route('/admin', methods=['GET', 'POST'])
@@ -910,6 +996,10 @@ def admin():
910
  title = request.form.get('title', '').strip()
911
  item_data = {'title': title, 'icon': request.form.get('icon'), 'description': request.form.get('description')}
912
  photo = request.files.get('photo')
 
 
 
 
913
  if action == 'add_service':
914
  if photo and photo.filename:
915
  item_data['photo'] = upload_photo_to_hf(photo, title, 'services')
@@ -936,6 +1026,10 @@ def admin():
936
  title = request.form.get('title', '').strip()
937
  item_data = {'title': title, 'description': request.form.get('description')}
938
  photo = request.files.get('photo')
 
 
 
 
939
  if action == 'add_project':
940
  if photo and photo.filename:
941
  item_data['photo'] = upload_photo_to_hf(photo, title, 'projects')
@@ -972,7 +1066,8 @@ def admin():
972
  equipment=data.get('equipment', []),
973
  categories=sorted(data.get('categories', [])),
974
  services=data.get('services', []),
975
- projects=data.get('projects', [])
 
976
  )
977
 
978
  def upload_photo_to_hf(photo, item_name, folder):
@@ -982,13 +1077,15 @@ def upload_photo_to_hf(photo, item_name, folder):
982
  api = HfApi()
983
  safe_name = secure_filename(item_name.replace(' ', '_'))[:50]
984
  ext = os.path.splitext(photo.filename)[1].lower()
 
985
  photo_filename = f"{safe_name}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{ext}"
986
 
987
  photo_file_obj = io.BytesIO(photo.read())
988
 
989
  api.upload_file(
990
  path_or_fileobj=photo_file_obj, path_in_repo=f"{folder}/{photo_filename}",
991
- repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE
 
992
  )
993
  logging.info(f"Uploaded photo {photo_filename} to {folder}")
994
  return photo_filename
@@ -1004,14 +1101,17 @@ def delete_photo_from_hf(photo_filename, folder):
1004
  api = HfApi()
1005
  api.delete_files(
1006
  repo_id=REPO_ID, paths_in_repo=[f"{folder}/{photo_filename}"],
1007
- repo_type="dataset", token=HF_TOKEN_WRITE
 
1008
  )
1009
  logging.info(f"Deleted photo {photo_filename} from {folder}")
1010
  except HfHubHTTPError as e:
1011
  if e.response.status_code != 404:
1012
- logging.error(f"Error deleting photo {photo_filename}: {e}")
 
 
1013
  except Exception as e:
1014
- logging.error(f"Error deleting photo {photo_filename}: {e}")
1015
 
1016
  @app.route('/force_upload', methods=['POST'])
1017
  def force_upload():
@@ -1021,14 +1121,19 @@ def force_upload():
1021
 
1022
  @app.route('/force_download', methods=['POST'])
1023
  def force_download():
1024
- download_db_from_hf()
1025
- flash("Данные скачаны с сервера.", 'success')
 
 
1026
  return redirect(url_for('admin'))
1027
 
1028
  if __name__ == '__main__':
1029
  logging.info("Application starting up...")
1030
- download_db_from_hf()
 
 
1031
  if HF_TOKEN_WRITE:
1032
  threading.Thread(target=periodic_backup, daemon=True).start()
 
1033
  port = int(os.environ.get('PORT', 7860))
1034
  app.run(debug=False, host='0.0.0.0', port=port)
 
144
  <title>ОсОО "Раина" - Вентиляция и Кондиционирование</title>
145
  <meta name="description" content="Профессиональные услуги по проектированию, монтажу и обслуживанию систем вентиляции и кондиционирования в Кыргызстане. 15 лет опыта, более 1000 проектов.">
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;
163
+ --button-border-radius: 30px;
164
  }
165
  * { margin: 0; padding: 0; box-sizing: border-box; scroll-behavior: smooth; }
166
+ body {
167
+ font-family: 'Montserrat', sans-serif;
168
+ background-color: var(--bg-dark);
169
+ color: var(--text-primary);
170
+ line-height: 1.7;
171
+ font-size: 16px;
172
  -webkit-font-smoothing: antialiased;
173
  -moz-osx-font-smoothing: grayscale;
174
  }
175
  .container { max-width: 1200px; margin: 0 auto; padding: 0 20px; }
176
+ section { padding: var(--section-padding) 0; overflow: hidden; }
177
+ h1, h2, h3 { font-weight: 700; color: var(--text-primary); line-height: 1.3; }
178
+ h1 { font-size: clamp(2.5rem, 7vw, 4.5rem); text-shadow: 0 2px 10px rgba(0,0,0,0.3); }
179
+ h2 {
180
+ font-size: clamp(2rem, 5vw, 3.2rem);
181
+ text-align: center;
182
+ margin-bottom: 70px;
183
+ position: relative;
184
  }
185
+ h2::after {
186
+ content: ''; display: block; width: 70px; height: 5px;
187
+ background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
188
+ margin: 20px auto 0;
189
+ border-radius: 3px;
 
 
 
190
  }
191
+ h3 { font-size: clamp(1.3rem, 3.5vw, 1.8rem); color: var(--accent-primary); margin-bottom: 15px; }
192
+ p { margin-bottom: 1.2rem; color: var(--text-secondary); font-size: 1.05rem; }
193
+ .btn {
194
+ display: inline-block;
195
+ padding: 14px 32px;
196
+ background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
197
+ color: #fff;
198
+ border-radius: var(--button-border-radius);
199
+ text-decoration: none;
200
+ font-weight: 600;
201
+ transition: all 0.3s ease;
202
+ box-shadow: 0 5px 20px var(--accent-glow);
203
+ border: none;
204
+ cursor: pointer;
205
  }
206
+ .btn:hover {
207
+ transform: translateY(-4px) scale(1.03);
208
+ box-shadow: 0 10px 30px var(--accent-glow);
209
+ background: linear-gradient(90deg, var(--accent-secondary), var(--accent-primary));
210
  }
211
+ .header {
212
+ position: fixed; top: 0; left: 0; width: 100%; z-index: 1000;
213
+ padding: 20px 0;
214
+ background-color: transparent;
215
+ transition: all 0.35s ease-out;
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
  }
223
  .navbar { display: flex; justify-content: space-between; align-items: center; }
224
+ .logo { font-size: clamp(1.6rem, 4vw, 2rem); font-weight: 700; color: #fff; text-decoration: none; letter-spacing: -1px; }
225
  .nav-links { display: flex; gap: 35px; list-style: none; }
226
+ .nav-links a {
227
+ color: var(--text-primary);
228
+ text-decoration: none;
229
+ font-weight: 500; /* Slightly less bold than 600 */
230
+ transition: color 0.3s ease, transform 0.3s ease;
231
+ padding-bottom: 5px;
232
+ position: relative;
233
+ }
234
  .nav-links a::after {
235
+ content: '';
236
+ position: absolute;
237
+ width: 0;
238
+ height: 2px;
239
+ bottom: 0;
240
+ left: 50%;
241
+ transform: translateX(-50%);
242
+ background-color: var(--accent-primary);
243
  transition: width 0.3s ease;
244
  }
245
+ .nav-links a:hover { color: var(--accent-primary); }
246
  .nav-links a:hover::after { width: 100%; }
247
+ .menu-toggle { display: none; font-size: 1.8rem; cursor: pointer; border: none; background: none; color: white; }
248
+
249
+ #hero {
250
+ min-height: 100vh;
251
+ display: flex; align-items: center;
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); }
262
+
263
+ .services-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 35px; }
264
+ .service-card, .turnkey-card {
265
+ background-color: var(--bg-medium);
266
+ padding: 35px;
267
+ border-radius: var(--card-border-radius);
268
+ border: 1px solid var(--border-color);
269
+ transition: all 0.3s ease;
270
+ box-shadow: 0 8px 25px rgba(0,0,0,0.15);
271
  }
272
+ .service-card:hover, .turnkey-card:hover {
273
+ transform: translateY(-8px);
274
+ border-color: var(--border-hover-color);
275
+ box-shadow: 0 12px 35px var(--accent-glow);
276
  }
277
+ .service-card i { font-size: 3rem; color: var(--accent-primary); margin-bottom: 25px; display: block; } /* Made icon larger and block */
278
+
279
+ .turnkey-card { padding: 0; display: flex; flex-direction: column; overflow: hidden; /* For image border radius */ }
280
+ .turnkey-img { width: 100%; height: 220px; object-fit: cover; border-radius: var(--card-border-radius) var(--card-border-radius) 0 0; transition: transform 0.3s ease; }
281
+ .turnkey-card:hover .turnkey-img { transform: scale(1.05); }
282
+ .turnkey-content { padding: 35px; flex-grow: 1;}
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); } /* Section specific bg */
288
+ #contact { background-color: var(--bg-medium); } /* Section specific bg */
289
+
290
+ .equipment-filters { display: flex; justify-content: center; flex-wrap: wrap; gap: 20px; margin-bottom: 50px; }
291
+ .filter-btn {
292
+ padding: 10px 25px;
293
+ border: 1px solid var(--accent-primary);
294
+ background-color: transparent;
295
+ color: var(--accent-primary);
296
+ border-radius: var(--button-border-radius);
297
+ cursor: pointer;
298
+ transition: all 0.3s ease;
299
+ font-weight: 500;
300
  }
301
+ .filter-btn.active, .filter-btn:hover { background-color: var(--accent-primary); color: #fff; }
302
+
303
+ .equipment-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 35px; }
304
+ .equipment-card {
305
+ background-color: var(--bg-light-card);
306
+ border-radius: var(--card-border-radius);
307
+ overflow: hidden;
308
+ text-align: center;
309
+ padding: 25px;
310
+ border: 1px solid var(--border-color);
311
+ transition: all 0.3s ease;
312
+ cursor: pointer;
313
  }
314
+ .equipment-card:hover {
315
+ transform: translateY(-8px);
316
+ border-color: var(--border-hover-color);
317
+ box-shadow: 0 10px 30px var(--accent-glow);
318
  }
319
+ .equipment-card img { width: 100%; height: 200px; object-fit: contain; margin-bottom: 20px; transition: transform 0.3s ease; }
 
 
320
  .equipment-card:hover img { transform: scale(1.05); }
321
+ .equipment-card h3 { font-size: 1.3rem; margin-bottom: 10px; color: var(--text-primary); }
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; }
367
+
368
  @media (max-width: 992px) {
369
  .grid-2 { grid-template-columns: 1fr; text-align: center; }
370
+ .about-img { margin-bottom: 40px; max-width: 500px; margin-left: auto; margin-right: auto;}
 
371
  }
372
  @media (max-width: 768px) {
373
+ .nav-links {
374
+ position: fixed; top: 0; right: -100%;
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); /* Swing effect */
379
+ box-shadow: -8px 0 25px rgba(0,0,0,0.25);
380
+ gap: 40px; /* Increased gap for mobile nav */
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; } /* Stack service cards on mobile */
388
+ .equipment-grid { grid-template-columns: 1fr; } /* Stack equipment cards on mobile */
389
+ .btn { padding: 12px 28px; }
390
  }
391
+
392
  .modal {
393
+ display: none;
394
+ position: fixed;
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); /* Darker overlay */
399
+ padding-top: 5vh; /* Responsive padding */
400
+ backdrop-filter: blur(5px);
401
  }
402
  .modal-content {
403
+ position: relative;
404
+ margin: 5% auto;
405
+ padding: 30px; /* Increased padding */
406
+ width: 90%;
407
+ max-width: 800px;
408
+ background-color: var(--bg-medium);
409
+ border-radius: var(--card-border-radius);
410
+ text-align: center;
411
+ box-shadow: 0 10px 40px rgba(0,0,0,0.5);
412
+ border: 1px solid var(--border-color);
413
+ }
414
+ .modal-content img {
415
+ max-width: 100%;
416
+ max-height: 65vh; /* Adjusted max height */
417
+ border-radius: 10px;
418
+ margin-bottom: 25px;
419
+ object-fit: contain;
420
  }
421
+ .modal-content h3 { margin-bottom: 15px; font-size: 1.8rem; color: var(--accent-primary); }
422
+ .modal-content p { color: var(--text-secondary); font-size: 1.1rem; line-height: 1.8; }
 
423
  .close-button {
424
+ position: absolute;
425
+ top: 20px;
426
+ right: 25px;
427
+ font-size: 2.8rem;
428
+ font-weight: bold;
429
+ color: var(--text-primary);
430
+ cursor: pointer;
431
+ background: none;
432
+ border: none;
433
+ transition: color 0.3s ease, transform 0.3s ease;
434
  }
435
+ .close-button:hover, .close-button:focus { color: var(--accent-primary); transform: rotate(90deg); }
436
+ .carousel-nav { margin-top: 25px; }
437
  .carousel-nav button {
438
+ background-color: var(--accent-primary);
439
+ color: white;
440
+ border: none;
441
+ padding: 12px 18px; /* Slightly larger padding */
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; /* Larger icons */
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); }
450
  </style>
451
  </head>
452
  <body>
 
474
  </section>
475
 
476
  <section id="about">
477
+ <div class="container">
478
  <h2>О Нашей Компании</h2>
479
  <div class="grid-2">
480
  <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">
 
491
  </section>
492
 
493
  <section id="services">
494
+ <div class="container">
495
  <h2>Наши Услуги</h2>
496
  <div class="services-grid">
497
+ <div class="service-card"><i class="fas fa-drafting-compass"></i><h3>Проектирование</h3><p>Точные расчеты, 3D-модели и вся необходимая проектная документация.</p></div>
498
+ <div class="service-card"><i class="fas fa-tools"></i><h3>Монтаж</h3><p>Профессиональная установка всех типов систем HVAC, от бытовых до промышленных.</p></div>
499
+ <div class="service-card"><i class="fas fa-headset"></i><h3>Сервис 24/7</h3><p>Плановое обслуживание и оперативный аварийный ремонт в любое время.</p></div>
500
+ <div class="service-card"><i class="fas fa-sync-alt"></i><h3>Модернизация</h3><p>Повышение энергоэффективности и снижение расходов на эксплуатацию.</p></div>
501
  </div>
502
  </div>
503
  </section>
504
 
505
  <section id="turnkey">
506
+ <div class="container">
507
  <h2>Услуги "под ключ"</h2>
508
  {% if services %}
509
  <div class="services-grid">
510
  {% for service in services %}
511
+ <div class="turnkey-card" onclick="showDetailsModal('service', {{ loop.index0 }})">
512
  {% if service.photo %}
513
  <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/services/{{ service.photo }}" alt="{{ service.title }}" class="turnkey-img">
514
  {% endif %}
515
  <div class="turnkey-content">
516
+ <h3><i class="{{ service.icon }} fa-fw" style="margin-right: 10px;"></i>{{ service.title }}</h3>
517
  <p>{{ service.description }}</p>
518
  </div>
519
  </div>
520
  {% endfor %}
521
  </div>
522
  {% else %}
523
+ <p style="text-align: center; color: var(--text-muted);">Информация об услугах "под ключ" скоро появится на сайте.</p>
524
  {% endif %}
525
  </div>
526
  </section>
527
 
528
  <section id="equipment">
529
+ <div class="container">
530
  <h2>Наше Оборудование</h2>
531
  {% if equipment %}
532
  <div class="equipment-filters">
 
537
  </div>
538
  <div class="equipment-grid">
539
  {% for item in equipment %}
540
+ <div class="equipment-card" data-category="{{ item.get('category', 'all') }}" onclick="showDetailsModal('equipment', {{ loop.index0 }})">
541
  {% if item.photo %}
542
  <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/equipment/{{ item.photo }}" alt="{{ item.name }}">
543
  {% else %}
544
+ <img src="https://via.placeholder.com/250x180.png?text=No+Image" alt="No Image" style="filter: grayscale(0.8) opacity(0.6);">
545
  {% endif %}
546
  <h3>{{ item.name }}</h3>
547
  <p class="price">{{ "%.2f"|format(item.price) }} KGS</p>
548
+ <a href="https://api.whatsapp.com/send?phone={{ whatsapp_phone }}&text=Здравствуйте, интересует оборудование: {{ item.name|urlencode }}" target="_blank" class="btn">Запросить</a>
549
  </div>
550
  {% endfor %}
551
  </div>
552
  {% else %}
553
+ <p style="text-align: center; color: var(--text-muted);">Каталог оборудования скоро будет доступен.</p>
554
  {% endif %}
555
  </div>
556
  </section>
557
 
558
  <section id="projects">
559
+ <div class="container">
560
  <h2>Реализованные Проекты</h2>
561
  {% if projects %}
562
  <div class="projects-grid">
 
571
  {% endfor %}
572
  </div>
573
  {% else %}
574
+ <p style="text-align: center; color: var(--text-muted);">Информация о реализованных проектах скоро появится на сайте.</p>
575
  {% endif %}
576
  </div>
577
  </section>
578
 
579
  <section id="contact">
580
+ <div class="container contact-content">
581
  <h2>Контакты</h2>
582
+ <p>Готовы стать вашим надежным партнером в создании идеального климата. Свяжитесь с нами для консультации или заказа услуг.</p>
583
  <div class="contact-info">
584
+ <p><strong>Телефон:</strong> <a href="tel:{{ contact_phone }}">{{ contact_phone }}</a></p>
585
+ <a href="https://api.whatsapp.com/send?phone={{ whatsapp_phone }}&text=Здравствуйте, я хотел(а) бы получить консультацию по вашим услугам." target="_blank" class="btn"><i class="fab fa-whatsapp"></i> Написать в WhatsApp</a>
586
  </div>
587
+ <div style="margin-top: 50px; font-size: 0.9rem; color: var(--text-muted);">
588
  <p><strong>Реквизиты:</strong> ОсОО «Раина», ИНН: 00812202110194, ОКПО: 31290279</p>
589
  </div>
590
  </div>
 
596
 
597
  <div id="detailsModal" class="modal">
598
  <div class="modal-content">
599
+ <button class="close-button" onclick="closeDetailsModal()" aria-label="Закрыть">×</button>
600
  <div id="modal-body"></div>
601
  <div class="carousel-nav">
602
+ <button id="prevBtn" onclick="changeModalItem(-1)" aria-label="Предыдущий">❮</button>
603
+ <button id="nextBtn" onclick="changeModalItem(1)" aria-label="Следующий">❯</button>
604
  </div>
605
  </div>
606
  </div>
 
612
  let allItems = [];
613
 
614
  function showDetailsModal(type, index) {
 
615
  const data = {{ data | tojson }};
616
  currentData = data;
617
  currentType = type;
618
+ currentIndex = index;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
619
 
620
+ if (type === 'service') allItems = data.services || [];
621
+ else if (type === 'equipment') allItems = data.equipment || [];
622
+ else if (type === 'project') allItems = data.projects || [];
623
+
624
  updateModalContent();
625
  document.getElementById('detailsModal').style.display = 'block';
626
  document.body.style.overflow = 'hidden';
627
  }
628
 
629
  function updateModalContent() {
630
+ if (!allItems || allItems.length === 0 || currentIndex < 0 || currentIndex >= allItems.length) {
631
+ // Optionally hide modal or show error if item not found
632
+ // closeDetailsModal();
633
+ return;
634
+ }
635
 
636
  const item = allItems[currentIndex];
637
  const modalBody = document.getElementById('modal-body');
638
+ modalBody.innerHTML = ''; // Clear previous content
639
  let content = '';
640
 
641
  if (currentType === 'service') {
642
  content = `
643
  ${item.photo ? `<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/services/${item.photo}" alt="${item.title}">` : ''}
644
+ <h3><i class="${item.icon || 'fas fa-tools'} fa-fw" style="margin-right: 10px; color: var(--accent-primary);"></i>${item.title || 'Услуга'}</h3>
645
+ <p>${item.description || 'Описание отсутствует.'}</p>
646
  `;
647
  } else if (currentType === 'equipment') {
648
  content = `
649
+ ${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">'}
650
+ <h3>${item.name || 'Оборудование'}</h3>
651
  <p><strong>Категория:</strong> ${item.category || 'Не указана'}</p>
652
+ <p class="price" style="font-size: 1.8rem; color: var(--accent-primary); margin: 20px 0;">${(item.price || 0).toFixed(2)} KGS</p>
653
+ <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>
654
  `;
655
  } else if (currentType === 'project') {
656
  content = `
657
+ ${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">'}
658
+ <h3>${item.title || 'Проект'}</h3>
659
+ <p>${item.description || 'Описание отсутствует.'}</p>
660
  `;
661
  }
662
  modalBody.innerHTML = content;
 
664
  }
665
 
666
  function changeModalItem(direction) {
667
+ if (!allItems || allItems.length === 0) return;
668
  let newIndex = currentIndex + direction;
669
  if (newIndex < 0) newIndex = allItems.length - 1;
670
  if (newIndex >= allItems.length) newIndex = 0;
 
675
  function updateCarouselNav() {
676
  const prevBtn = document.getElementById('prevBtn');
677
  const nextBtn = document.getElementById('nextBtn');
678
+ if (!allItems || allItems.length <= 1) {
679
  prevBtn.style.display = 'none';
680
  nextBtn.style.display = 'none';
681
  } else {
682
  prevBtn.style.display = 'inline-block';
683
  nextBtn.style.display = 'inline-block';
684
+ prevBtn.disabled = currentIndex === 0;
685
+ nextBtn.disabled = currentIndex === allItems.length - 1;
686
  }
687
  }
688
 
689
  function closeDetailsModal() {
690
  document.getElementById('detailsModal').style.display = 'none';
691
  document.body.style.overflow = '';
692
+ // Reset state if needed, but not strictly necessary to clear currentData/Type/Index here
693
+ // as they get overwritten on next open.
 
 
694
  }
695
+
696
+ // Close modal on ESC key
697
+ document.addEventListener('keydown', function(event) {
698
+ if (event.key === 'Escape' || event.key === 'Esc') {
699
+ if (document.getElementById('detailsModal').style.display === 'block') {
700
+ closeDetailsModal();
701
+ }
702
+ }
703
+ });
704
+
705
+ // Close modal on clicking outside the content
706
+ document.getElementById('detailsModal').addEventListener('click', function(event) {
707
+ if (event.target === this) { // 'this' refers to the modal itself
708
+ closeDetailsModal();
709
+ }
710
+ });
711
+
712
 
713
  document.addEventListener('DOMContentLoaded', function() {
714
  const header = document.querySelector('.header');
 
721
 
722
  menuToggle.addEventListener('click', () => {
723
  navLinks.classList.toggle('active');
724
+ menuToggle.setAttribute('aria-expanded', navLinks.classList.contains('active'));
725
  });
726
 
727
  document.querySelectorAll('.nav-links a').forEach(link => {
728
  link.addEventListener('click', () => {
729
  navLinks.classList.remove('active');
730
+ menuToggle.setAttribute('aria-expanded', 'false');
731
  });
732
  });
733
 
 
735
  if (filterContainer) {
736
  filterContainer.addEventListener('click', (e) => {
737
  if (!e.target.matches('.filter-btn')) return;
738
+
739
+ const currentActive = filterContainer.querySelector('.filter-btn.active');
740
+ if(currentActive) currentActive.classList.remove('active');
741
+
742
  e.target.classList.add('active');
743
  const filter = e.target.dataset.filter;
744
+
745
  document.querySelectorAll('.equipment-card').forEach(card => {
746
+ card.style.display = (filter === 'all' || card.dataset.category === filter) ? 'block' : 'none';
747
  });
748
  });
749
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
750
  });
751
  </script>
752
  </body>
 
933
  contact_phone=CONTACT_PHONE,
934
  whatsapp_phone=WHATSAPP_PHONE,
935
  now=datetime.utcnow(),
936
+ data=data
937
  )
938
 
939
  @app.route('/admin', methods=['GET', 'POST'])
 
996
  title = request.form.get('title', '').strip()
997
  item_data = {'title': title, 'icon': request.form.get('icon'), 'description': request.form.get('description')}
998
  photo = request.files.get('photo')
999
+ if not title:
1000
+ flash("Заголовок услуги обязателен.", 'error')
1001
+ return redirect(url_for('admin'))
1002
+
1003
  if action == 'add_service':
1004
  if photo and photo.filename:
1005
  item_data['photo'] = upload_photo_to_hf(photo, title, 'services')
 
1026
  title = request.form.get('title', '').strip()
1027
  item_data = {'title': title, 'description': request.form.get('description')}
1028
  photo = request.files.get('photo')
1029
+ if not title:
1030
+ flash("Заголовок проекта обязателен.", 'error')
1031
+ return redirect(url_for('admin'))
1032
+
1033
  if action == 'add_project':
1034
  if photo and photo.filename:
1035
  item_data['photo'] = upload_photo_to_hf(photo, title, 'projects')
 
1066
  equipment=data.get('equipment', []),
1067
  categories=sorted(data.get('categories', [])),
1068
  services=data.get('services', []),
1069
+ projects=data.get('projects', []),
1070
+ repo_id=REPO_ID
1071
  )
1072
 
1073
  def upload_photo_to_hf(photo, item_name, folder):
 
1077
  api = HfApi()
1078
  safe_name = secure_filename(item_name.replace(' ', '_'))[:50]
1079
  ext = os.path.splitext(photo.filename)[1].lower()
1080
+ if not ext: ext = ".jpg"
1081
  photo_filename = f"{safe_name}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{ext}"
1082
 
1083
  photo_file_obj = io.BytesIO(photo.read())
1084
 
1085
  api.upload_file(
1086
  path_or_fileobj=photo_file_obj, path_in_repo=f"{folder}/{photo_filename}",
1087
+ repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE,
1088
+ commit_message=f"Upload photo {photo_filename} for {folder}"
1089
  )
1090
  logging.info(f"Uploaded photo {photo_filename} to {folder}")
1091
  return photo_filename
 
1101
  api = HfApi()
1102
  api.delete_files(
1103
  repo_id=REPO_ID, paths_in_repo=[f"{folder}/{photo_filename}"],
1104
+ repo_type="dataset", token=HF_TOKEN_WRITE,
1105
+ commit_message=f"Delete photo {photo_filename} from {folder}"
1106
  )
1107
  logging.info(f"Deleted photo {photo_filename} from {folder}")
1108
  except HfHubHTTPError as e:
1109
  if e.response.status_code != 404:
1110
+ logging.error(f"HTTP error deleting photo {photo_filename}: {e}")
1111
+ else:
1112
+ logging.info(f"Photo {photo_filename} not found on HF for deletion, or already deleted.")
1113
  except Exception as e:
1114
+ logging.error(f"Unexpected error deleting photo {photo_filename}: {e}")
1115
 
1116
  @app.route('/force_upload', methods=['POST'])
1117
  def force_upload():
 
1121
 
1122
  @app.route('/force_download', methods=['POST'])
1123
  def force_download():
1124
+ if download_db_from_hf():
1125
+ flash("Данные успешно скачаны с сервера.", 'success')
1126
+ else:
1127
+ flash("Ошибка при скачивании данных с сервера.", 'error')
1128
  return redirect(url_for('admin'))
1129
 
1130
  if __name__ == '__main__':
1131
  logging.info("Application starting up...")
1132
+ if not download_db_from_hf():
1133
+ logging.warning("Initial database download failed. Application might start with empty or outdated data.")
1134
+
1135
  if HF_TOKEN_WRITE:
1136
  threading.Thread(target=periodic_backup, daemon=True).start()
1137
+
1138
  port = int(os.environ.get('PORT', 7860))
1139
  app.run(debug=False, host='0.0.0.0', port=port)