Kgshop commited on
Commit
55597e8
·
verified ·
1 Parent(s): 2ac7066

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +399 -464
app.py CHANGED
@@ -1,4 +1,4 @@
1
- from flask import Flask, render_template_string, request, redirect, url_for, send_file, flash, jsonify
2
  import json
3
  import os
4
  import logging
@@ -10,12 +10,11 @@ from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError
10
  from werkzeug.utils import secure_filename
11
  from dotenv import load_dotenv
12
  import requests
13
- import uuid
14
 
15
  load_dotenv()
16
 
17
  app = Flask(__name__)
18
- app.secret_key = 'raina_hvac_secret_key_98765_landing_v2'
19
  DATA_FILE = 'data.json'
20
 
21
  SYNC_FILES = [DATA_FILE]
@@ -44,80 +43,61 @@ def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWN
44
  for attempt in range(retries + 1):
45
  try:
46
  logging.info(f"Downloading {file_name} (Attempt {attempt + 1}/{retries + 1})...")
47
- local_path = hf_hub_download(
48
- repo_id=REPO_ID,
49
- filename=file_name,
50
- repo_type="dataset",
51
- token=token_to_use,
52
- local_dir=".",
53
- local_dir_use_symlinks=False,
54
- force_download=True,
55
- resume_download=False
56
  )
57
- logging.info(f"Successfully downloaded {file_name} to {local_path}.")
58
  success = True
59
  break
60
  except RepositoryNotFoundError:
61
- logging.error(f"Repository {REPO_ID} not found. Download cancelled for all files.")
62
  return False
63
  except HfHubHTTPError as e:
64
  if e.response.status_code == 404:
65
- logging.warning(f"File {file_name} not found in repo {REPO_ID} (404). Skipping this file.")
66
- if attempt == 0 and not os.path.exists(file_name):
67
  try:
68
- if file_name == DATA_FILE:
69
- with open(file_name, 'w', encoding='utf-8') as f:
70
- json.dump({'products': [], 'categories': [], 'services': [], 'projects': []}, f)
71
- logging.info(f"Created empty local file {file_name} because it was not found on HF.")
72
  except Exception as create_e:
73
  logging.error(f"Failed to create empty local file {file_name}: {create_e}")
74
- success = False
75
  break
76
  else:
77
- logging.error(f"HTTP error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...")
78
- except requests.exceptions.RequestException as e:
79
- logging.error(f"Network error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...")
80
  except Exception as e:
81
- logging.error(f"Unexpected error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...", exc_info=True)
82
  if attempt < retries:
83
  time.sleep(delay)
84
  if not success:
85
  logging.error(f"Failed to download {file_name} after {retries + 1} attempts.")
86
  all_successful = False
87
- logging.info(f"Download process finished. Overall success: {all_successful}")
88
  return all_successful
89
 
90
  def upload_db_to_hf(specific_file=None):
91
  if not HF_TOKEN_WRITE:
92
- logging.warning("HF_TOKEN (for writing) not set. Skipping upload to Hugging Face.")
93
  return
94
  try:
95
  api = HfApi()
96
  files_to_upload = [specific_file] if specific_file else SYNC_FILES
97
- logging.info(f"Starting upload of {files_to_upload} to HF repo {REPO_ID}...")
98
  for file_name in files_to_upload:
99
  if os.path.exists(file_name):
100
- try:
101
- api.upload_file(
102
- path_or_fileobj=file_name,
103
- path_in_repo=file_name,
104
- repo_id=REPO_ID,
105
- repo_type="dataset",
106
- token=HF_TOKEN_WRITE,
107
- commit_message=f"Sync {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
108
- )
109
- logging.info(f"File {file_name} successfully uploaded to Hugging Face.")
110
- except Exception as e:
111
- logging.error(f"Error uploading file {file_name} to Hugging Face: {e}")
112
  else:
113
  logging.warning(f"File {file_name} not found locally, skipping upload.")
114
- logging.info("Finished uploading files to HF.")
115
  except Exception as e:
116
- logging.error(f"General error during Hugging Face upload initialization or process: {e}", exc_info=True)
117
 
118
  def periodic_backup():
119
  backup_interval = 1800
120
- logging.info(f"Setting up periodic backup every {backup_interval} seconds.")
121
  while True:
122
  time.sleep(backup_interval)
123
  logging.info("Starting periodic backup...")
@@ -125,60 +105,34 @@ def periodic_backup():
125
  logging.info("Periodic backup finished.")
126
 
127
  def load_data():
128
- default_data = {'products': [], 'categories': [], 'services': [], 'projects': []}
129
  try:
130
  with open(DATA_FILE, 'r', encoding='utf-8') as file:
131
  data = json.load(file)
132
- logging.info(f"Local data loaded successfully from {DATA_FILE}")
133
  if not isinstance(data, dict):
134
- raise FileNotFoundError
135
- if 'products' not in data: data['products'] = []
136
  if 'categories' not in data: data['categories'] = []
137
  if 'services' not in data: data['services'] = []
138
  if 'projects' not in data: data['projects'] = []
139
  return data
140
- except (FileNotFoundError, json.JSONDecodeError):
141
- logging.warning(f"Local file {DATA_FILE} not found or corrupt. Attempting download from HF.")
142
- if download_db_from_hf(specific_file=DATA_FILE):
143
- try:
144
- with open(DATA_FILE, 'r', encoding='utf-8') as file:
145
- data = json.load(file)
146
- logging.info(f"Data loaded successfully from {DATA_FILE} after download.")
147
- if not isinstance(data, dict): return default_data
148
- if 'products' not in data: data['products'] = []
149
- if 'categories' not in data: data['categories'] = []
150
- if 'services' not in data: data['services'] = []
151
- if 'projects' not in data: data['projects'] = []
152
- return data
153
- except Exception as e:
154
- logging.error(f"Unknown error loading downloaded {DATA_FILE}: {e}. Using default.", exc_info=True)
155
- return default_data
156
- else:
157
- logging.error(f"Failed to download {DATA_FILE} from HF. Using/creating empty local data.")
158
- if not os.path.exists(DATA_FILE):
159
- try:
160
- with open(DATA_FILE, 'w', encoding='utf-8') as f:
161
- json.dump(default_data, f)
162
- logging.info(f"Created empty local file {DATA_FILE} after failed download.")
163
- except Exception as create_e:
164
- logging.error(f"Failed to create empty local file {DATA_FILE}: {create_e}")
165
  return default_data
166
 
167
  def save_data(data):
168
  try:
169
  if not isinstance(data, dict):
170
- logging.error("Attempted to save invalid data structure (not a dict). Aborting save.")
171
  return
172
- if 'products' not in data: data['products'] = []
173
- if 'categories' not in data: data['categories'] = []
174
- if 'services' not in data: data['services'] = []
175
- if 'projects' not in data: data['projects'] = []
176
  with open(DATA_FILE, 'w', encoding='utf-8') as file:
177
  json.dump(data, file, ensure_ascii=False, indent=4)
178
- logging.info(f"Data successfully saved to {DATA_FILE}")
179
  upload_db_to_hf(specific_file=DATA_FILE)
180
  except Exception as e:
181
- logging.error(f"Error saving data to {DATA_FILE}: {e}", exc_info=True)
182
 
183
  LANDING_TEMPLATE = '''
184
  <!DOCTYPE html>
@@ -192,136 +146,93 @@ LANDING_TEMPLATE = '''
192
  <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap" rel="stylesheet">
193
  <style>
194
  :root {
195
- --dark-bg: #12121c;
196
- --card-bg: #1a1a2e;
197
- --primary-color: #a955ff;
198
- --secondary-color: #6a0dad;
199
- --text-color: #e0e0e0;
200
- --text-muted: #a0a0b0;
201
  --accent-glow: rgba(169, 85, 255, 0.3);
202
  }
203
  * { margin: 0; padding: 0; box-sizing: border-box; scroll-behavior: smooth; }
204
  body { font-family: 'Montserrat', sans-serif; background-color: var(--dark-bg); color: var(--text-color); line-height: 1.7; font-size: 16px; }
205
  .container { max-width: 1200px; margin: 0 auto; padding: 0 20px; }
206
- section { padding: 80px 0; }
207
  h1, h2, h3 { font-weight: 700; color: #fff; line-height: 1.3; }
208
- h1 { font-size: clamp(2.5rem, 6vw, 4rem); }
209
  h2 { font-size: clamp(2rem, 5vw, 3rem); text-align: center; margin-bottom: 60px; position: relative; }
210
  h2::after { content: ''; display: block; width: 80px; height: 4px; background: linear-gradient(90deg, var(--primary-color), var(--secondary-color)); margin: 15px auto 0; border-radius: 2px; }
211
  h3 { font-size: clamp(1.2rem, 3vw, 1.5rem); color: var(--primary-color); margin-bottom: 15px; }
212
  p { margin-bottom: 1rem; color: var(--text-muted); }
213
  .btn { display: inline-block; padding: 12px 28px; background: linear-gradient(90deg, var(--primary-color), var(--secondary-color)); color: #fff; border-radius: 50px; text-decoration: none; font-weight: 600; transition: all 0.3s ease; box-shadow: 0 4px 15px var(--accent-glow); }
214
  .btn:hover { transform: translateY(-3px) scale(1.05); box-shadow: 0 8px 25px var(--accent-glow); }
215
-
216
- .header { position: fixed; top: 0; left: 0; width: 100%; z-index: 1000; padding: 15px 0; background-color: rgba(18, 18, 28, 0.8); backdrop-filter: blur(10px); transition: all 0.3s ease; }
217
  .header.scrolled { padding: 10px 0; box-shadow: 0 2px 10px rgba(0,0,0,0.3); }
218
  .navbar { display: flex; justify-content: space-between; align-items: center; }
219
- .logo { font-size: 1.8rem; font-weight: 700; color: #fff; text-decoration: none; }
220
- .nav-links { display: none; list-style: none; }
221
- .menu-toggle { display: block; font-size: 1.5rem; cursor: pointer; color: #fff; }
222
-
223
- #hero { min-height: 100vh; display: flex; align-items: center; background-image: linear-gradient(rgba(18, 18, 28, 0.7), rgba(18, 18, 28, 1)), url(https://i.imgur.com/k6O6XbJ.jpeg); background-size: cover; background-position: center; }
 
224
  .hero-content { text-align: center; max-width: 800px; margin: 0 auto; }
225
  .hero-content p { font-size: clamp(1rem, 2.5vw, 1.2rem); margin: 30px 0; max-width: 600px; margin-left: auto; margin-right: auto;}
226
-
227
- .grid-2 { display: grid; grid-template-columns: 1fr; gap: 40px; align-items: center; }
228
- .about-img { width: 100%; border-radius: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.4); max-width: 500px; margin: 0 auto 30px auto;}
229
-
230
- .services-grid, .projects-grid { display: grid; grid-template-columns: 1fr; gap: 30px; }
231
  .service-card, .turnkey-card { background-color: var(--card-bg); padding: 30px; border-radius: 15px; border: 1px solid #2a2a4a; transition: all 0.3s ease; }
232
  .service-card:hover, .turnkey-card:hover { transform: translateY(-5px); border-color: var(--primary-color); box-shadow: 0 8px 25px var(--accent-glow); }
233
  .service-card i { font-size: 2.5rem; color: var(--primary-color); margin-bottom: 20px; }
234
-
235
- .turnkey-card { display: flex; flex-direction: column; }
236
- .turnkey-img { width: 100%; height: 200px; object-fit: cover; border-radius: 10px 10px 0 0;}
237
  .turnkey-content { padding: 30px; flex-grow: 1;}
238
-
239
- .equipment-filters { display: flex; justify-content: center; flex-wrap: wrap; gap: 10px; margin-bottom: 40px; }
240
- .filter-btn { padding: 8px 18px; border: 1px solid var(--primary-color); background-color: transparent; color: var(--primary-color); border-radius: 20px; cursor: pointer; transition: all 0.3s; font-size: 0.9rem;}
241
  .filter-btn.active, .filter-btn:hover { background-color: var(--primary-color); color: #fff; }
242
- .equipment-grid { display: grid; grid-template-columns: 1fr; gap: 30px; }
243
  .equipment-card { background-color: var(--card-bg); border-radius: 15px; overflow: hidden; text-align: center; padding: 20px; border: 1px solid #2a2a4a; transition: all 0.3s ease; }
244
  .equipment-card img { width: 100%; height: 180px; object-fit: contain; margin-bottom: 15px; }
245
  .equipment-card h3 { font-size: 1.2rem; }
246
  .equipment-card .price { font-size: 1.3rem; font-weight: 700; color: #fff; margin: 10px 0; }
247
-
248
- .project-card { position: relative; border-radius: 15px; overflow: hidden; aspect-ratio: 4 / 3; }
249
  .project-card img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform 0.4s ease; }
250
- .project-overlay { position: absolute; bottom: 0; left: 0; right: 0; background: linear-gradient(to top, rgba(18,18,28,1) 0%, rgba(18,18,28,0) 100%); padding: 40px 20px 20px; transition: all 0.4s ease; }
251
- .project-card h3 { margin-bottom: 5px; font-size: clamp(1.1rem, 4vw, 1.3rem); }
252
- .project-card p { margin-bottom: 0; transition: opacity 0.4s ease, max-height 0.4s ease; opacity: 0; max-height: 0; overflow: hidden; }
253
  .project-card:hover img { transform: scale(1.05); }
254
  .project-card:hover p { opacity: 1; max-height: 200px; }
255
-
256
  #contact { background-color: var(--card-bg); }
257
  .contact-content { text-align: center; }
258
  .contact-info { margin-top: 40px; display: flex; flex-direction: column; align-items: center; gap: 20px; }
259
  .contact-info p { font-size: 1.2rem; margin-bottom: 0; }
260
  .contact-info a { color: var(--primary-color); text-decoration: none; font-weight: 600; }
261
  .footer { text-align: center; padding: 30px 0; background-color: #0d0d14; }
262
-
263
- @media (min-width: 576px) {
264
- .services-grid { grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }
265
- .equipment-grid { grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); }
266
- .projects-grid { grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }
267
- }
268
- @media (min-width: 992px) {
269
- .nav-links { display: flex; gap: 30px; }
270
- .menu-toggle { display: none; }
271
- .grid-2 { grid-template-columns: 1fr 1fr; }
272
- .about-img { margin: 0; }
273
  }
274
- .mobile-nav {
275
- position: fixed;
276
- top: 0;
277
- left: -100%;
278
- width: 80%;
279
- max-width: 300px;
280
- height: 100vh;
281
- background-color: var(--card-bg);
282
- box-shadow: 5px 0 15px rgba(0,0,0,0.2);
283
- z-index: 1001;
284
- transition: left 0.4s ease-in-out;
285
- display: flex;
286
- flex-direction: column;
287
- justify-content: center;
288
- align-items: center;
289
- padding: 20px;
290
  }
291
- .mobile-nav.active { left: 0; }
292
- .mobile-nav .nav-links { display: flex; flex-direction: column; align-items: center; gap: 25px; padding: 0; }
293
- .mobile-nav .nav-links a { font-size: 1.2rem; }
294
- #nav-close-btn { position: absolute; top: 20px; right: 20px; font-size: 1.8rem; color: #fff; cursor: pointer; }
295
  </style>
296
  </head>
297
  <body>
298
  <header class="header">
299
  <div class="container navbar">
300
  <a href="#" class="logo">Раина</a>
301
- <nav class="nav-links">
302
- <a href="#about">О компании</a>
303
- <a href="#services">Услуги</a>
304
- <a href="#turnkey">Под ключ</a>
305
- <a href="#equipment">Оборудование</a>
306
- <a href="#projects">Проекты</a>
307
- <a href="#contact">Контакты</a>
308
- </nav>
309
- <div class="menu-toggle"><i class="fas fa-bars"></i></div>
310
  </div>
311
  </header>
312
 
313
- <div class="mobile-nav">
314
- <div id="nav-close-btn"><i class="fas fa-times"></i></div>
315
- <ul class="nav-links">
316
- <li><a href="#about">О компании</a></li>
317
- <li><a href="#services">Услуги</a></li>
318
- <li><a href="#turnkey">Под ключ</a></li>
319
- <li><a href="#equipment">Оборудование</a></li>
320
- <li><a href="#projects">Проекты</a></li>
321
- <li><a href="#contact">Контакты</a></li>
322
- </ul>
323
- </div>
324
-
325
  <section id="hero">
326
  <div class="container hero-content">
327
  <h1>ОсОО "Раина": Ваш Партнер в Вентиляции и Кондиционировании</h1>
@@ -334,14 +245,14 @@ LANDING_TEMPLATE = '''
334
  <div class="container">
335
  <h2>О Нашей Компании</h2>
336
  <div class="grid-2">
337
- <img src="https://i.imgur.com/8QhV42S.jpeg" alt="Команда Раина" class="about-img">
338
  <div>
339
  <h3>Основание и История</h3>
340
  <p>Компания "Раина" была основана в 2009 году. За эти годы мы зарекомендовали себя как надежный партнер, стремящийся к инновациям и совершенству в области климатических решений.</p>
341
  <h3>Наша Миссия</h3>
342
  <p>Наша миссия — создание оптимального микроклимата для наших клиентов, обеспечивающего комфорт, здоровье и высокую производительность.</p>
343
  <h3>Профессиональная Команда</h3>
344
- <p>Наша команда состоит из высококвалифицированных и сертифицированных инженеров и техников, обладающих глубокими знаниями и опытом в области HVAC.</p>
345
  </div>
346
  </div>
347
  </div>
@@ -353,20 +264,22 @@ LANDING_TEMPLATE = '''
353
  <div class="services-grid">
354
  <div class="service-card"><i class="fas fa-drafting-compass"></i><h3>Проектирование</h3><p>Точные расчеты, 3D-модели и вся необходимая проектная документация.</p></div>
355
  <div class="service-card"><i class="fas fa-tools"></i><h3>Монтаж</h3><p>Профессиональная установка всех типов систем HVAC, от бытовых до промышленных.</p></div>
356
- <div class="service-card"><i class="fas fa-headset"></i><h3>Сервис 24/7</h3><p>Полный спектр услуг по плановому обслуживанию и аварийному ремонту.</p></div>
357
  <div class="service-card"><i class="fas fa-sync-alt"></i><h3>Модернизация</h3><p>Повышение энергоэффективности и снижение расходов на эксплуатацию.</p></div>
358
  </div>
359
  </div>
360
  </section>
361
 
362
- <section id="turnkey">
363
  <div class="container">
364
  <h2>Услуги "под ключ"</h2>
365
  {% if services %}
366
  <div class="services-grid">
367
  {% for service in services %}
368
  <div class="turnkey-card">
369
- {% if service.photo %}<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/services/{{ service.photo }}" alt="{{ service.title }}" class="turnkey-img">{% endif %}
 
 
370
  <div class="turnkey-content">
371
  <h3><i class="{{ service.icon }} fa-fw" style="margin-right: 8px; color: var(--primary-color);"></i>{{ service.title }}</h3>
372
  <p>{{ service.description }}</p>
@@ -375,26 +288,32 @@ LANDING_TEMPLATE = '''
375
  {% endfor %}
376
  </div>
377
  {% else %}
378
- <p style="text-align: center;">Информация об услугах "под ключ" скоро появится.</p>
379
  {% endif %}
380
  </div>
381
  </section>
382
 
383
- <section id="equipment" style="background-color: var(--card-bg);">
384
  <div class="container">
385
  <h2>Наше Оборудование</h2>
386
- {% if products %}
387
  <div class="equipment-filters">
388
  <button class="filter-btn active" data-filter="all">Все</button>
389
- {% for category in categories %}<button class="filter-btn" data-filter="{{ category }}">{{ category }}</button>{% endfor %}
 
 
390
  </div>
391
  <div class="equipment-grid">
392
- {% for product in products %}
393
- <div class="equipment-card" data-category="{{ product.get('category', 'all') }}">
394
- <img src="{% if product.photos %}https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product.photos[0] }}{% else %}https://via.placeholder.com/250x180.png?text=No+Image{% endif %}" alt="{{ product.name }}">
395
- <h3>{{ product.name }}</h3>
396
- <p class="price">{{ "%.2f"|format(product.price) }} KGS</p>
397
- <a href="https://api.whatsapp.com/send?phone={{ whatsapp_phone }}&text=Здравствуйте, интересует оборудование: {{ product.name }}" target="_blank" class="btn" style="padding: 8px 20px; font-size: 0.9rem;">Запросить</a>
 
 
 
 
398
  </div>
399
  {% endfor %}
400
  </div>
@@ -403,7 +322,7 @@ LANDING_TEMPLATE = '''
403
  {% endif %}
404
  </div>
405
  </section>
406
-
407
  <section id="projects">
408
  <div class="container">
409
  <h2>Реализованные Проекты</h2>
@@ -411,7 +330,7 @@ LANDING_TEMPLATE = '''
411
  <div class="projects-grid">
412
  {% for project in projects %}
413
  <div class="project-card">
414
- <img src="{% if project.photo %}https://huggingface.co/datasets/{{ repo_id }}/resolve/main/projects/{{ project.photo }}{% else %}https://i.imgur.com/k6O6XbJ.jpeg{% endif %}" alt="{{ project.title }}">
415
  <div class="project-overlay">
416
  <h3>{{ project.title }}</h3>
417
  <p>{{ project.description }}</p>
@@ -420,63 +339,49 @@ LANDING_TEMPLATE = '''
420
  {% endfor %}
421
  </div>
422
  {% else %}
423
- <p style="text-align: center;">Информация о реализованных проектах скоро появится.</p>
424
  {% endif %}
425
  </div>
426
  </section>
427
 
428
  <section id="contact">
429
  <div class="container contact-content">
430
- <h2>Контакты и Следующие Шаги</h2>
431
- <p>Мы готовы стать вашим надежным партнером в создании идеального климата.</p>
432
  <div class="contact-info">
433
  <p><strong>Свяжитесь с нами:</strong> <a href="tel:{{ contact_phone }}">{{ contact_phone }}</a></p>
434
  <a href="https://api.whatsapp.com/send?phone={{ whatsapp_phone }}&text=Здравствуйте, я хотел(а) бы получить консультацию по вашим услугам." target="_blank" class="btn"><i class="fab fa-whatsapp"></i> Написать в WhatsApp</a>
435
  </div>
436
  <div style="margin-top: 40px; font-size: 0.9rem; color: var(--text-muted);">
437
  <p><strong>Реквизиты:</strong> ОсОО «Раина», ИНН: 00812202110194, ОКПО: 31290279</p>
438
- <p><strong>Банк:</strong> Центральный филиал ОАО «Бакай Банк», БИК: 124030, Счет(KGS): 1240020000834408</p>
439
  </div>
440
  </div>
441
  </section>
442
 
443
  <footer class="footer">
444
- <div class="container">
445
- <p>© {{ now.year }} ОсОО "Раина". Все права защищены.</p>
446
- </div>
447
  </footer>
448
 
449
  <script>
450
  document.addEventListener('DOMContentLoaded', function() {
451
  const header = document.querySelector('.header');
452
  const menuToggle = document.querySelector('.menu-toggle');
453
- const mobileNav = document.querySelector('.mobile-nav');
454
- const navCloseBtn = document.getElementById('nav-close-btn');
455
-
456
- window.addEventListener('scroll', () => {
457
- header.classList.toggle('scrolled', window.scrollY > 50);
458
- });
459
-
460
- const closeNav = () => mobileNav.classList.remove('active');
461
- const openNav = () => mobileNav.classList.add('active');
462
-
463
- menuToggle.addEventListener('click', openNav);
464
- navCloseBtn.addEventListener('click', closeNav);
465
- mobileNav.querySelectorAll('a').forEach(link => {
466
- link.addEventListener('click', closeNav);
467
  });
468
-
469
  const filterContainer = document.querySelector('.equipment-filters');
470
  if (filterContainer) {
471
  filterContainer.addEventListener('click', (e) => {
472
- if (e.target.classList.contains('filter-btn')) {
473
- filterContainer.querySelector('.active').classList.remove('active');
474
- e.target.classList.add('active');
475
- const filter = e.target.getAttribute('data-filter');
476
- document.querySelectorAll('.equipment-card').forEach(item => {
477
- item.style.display = (filter === 'all' || item.getAttribute('data-category') === filter) ? 'block' : 'none';
478
- });
479
- }
480
  });
481
  }
482
  });
@@ -486,132 +391,165 @@ LANDING_TEMPLATE = '''
486
  '''
487
 
488
  ADMIN_TEMPLATE = '''
489
- <!DOCTYPE html>
490
- <html lang="ru">
491
- <head>
492
- <meta charset="UTF-8">
493
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
494
- <title>Админ-панель - Раина</title>
495
- <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
496
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
497
- <style>
498
- body { font-family: 'Poppins', sans-serif; background-color: #f4f7f9; color: #333; padding: 20px; line-height: 1.6; }
499
- .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); }
500
- .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;}
501
- h1, h2, h3 { font-weight: 600; color: #6a0dad; margin-bottom: 15px; }
502
- h1 { font-size: 1.8rem; }
503
- h2 { font-size: 1.5rem; margin-top: 30px; display: flex; align-items: center; gap: 8px; }
504
- h3 { font-size: 1.2rem; color: #8e44ad; margin-top: 20px; }
505
- .section { margin-bottom: 30px; padding: 20px; background-color: #fafafa; border: 1px solid #e9e9e9; border-radius: 8px; }
506
- form { margin-bottom: 20px; }
507
- label { font-weight: 500; margin-top: 10px; display: block; color: #555; font-size: 0.9rem;}
508
- input[type="text"], input[type="number"], textarea, select { width: 100%; padding: 10px 12px; margin-top: 5px; border: 1px solid #ddd; border-radius: 6px; font-size: 0.95rem; box-sizing: border-box; transition: border-color 0.3s ease; background-color: #fff; }
509
- input:focus, textarea:focus, select:focus { border-color: #8e44ad; outline: none; box-shadow: 0 0 0 2px rgba(142, 68, 173, 0.1); }
510
- textarea { min-height: 80px; resize: vertical; }
511
- input[type="file"] { padding: 8px; background-color: #ffffff; cursor: pointer; border: 1px solid #ddd;}
512
- button, .button { padding: 10px 18px; border: none; border-radius: 6px; background-color: #9b59b6; color: white; font-weight: 500; cursor: pointer; transition: all 0.3s ease; margin-top: 15px; font-size: 0.95rem; display: inline-flex; align-items: center; gap: 5px; text-decoration: none; }
513
- button:hover, .button:hover { background-color: #8e44ad; transform: translateY(-2px); }
514
- button:active { transform: scale(0.98); }
515
- .delete-button { background-color: #e74c3c; }
516
- .delete-button:hover { background-color: #c0392b; }
517
- .item-list { display: grid; gap: 20px; }
518
- .item { background: #fff; padding: 15px 20px; border-radius: 8px; border: 1px solid #eee; }
519
- .item-actions { margin-top: 15px; display: flex; gap: 10px; flex-wrap: wrap; }
520
- .edit-form-container { margin-top: 15px; padding: 20px; background: #fdf9ff; border: 1px dashed #ddd; border-radius: 6px; display: none; }
521
- details { background-color: #fafafa; border: 1px solid #e9e9e9; border-radius: 8px; margin-bottom: 20px; }
522
- details > summary { cursor: pointer; font-weight: 600; color: #8e44ad; display: block; padding: 15px; position: relative; list-style: none; }
523
- details > summary::after { content: '\\f078'; font-family: 'Font Awesome 6 Free'; font-weight: 900; position: absolute; right: 20px; top: 50%; transform: translateY(-50%); transition: transform 0.2s ease; }
524
- details[open] > summary { border-bottom: 1px solid #e9e9e9; }
525
- details[open] > summary::after { transform: translateY(-50%) rotate(180deg); }
526
- .form-content { padding: 20px; }
527
- .photo-preview img { max-width: 70px; max-height: 70px; border-radius: 5px; margin: 5px 5px 0 0; border: 1px solid #e0e0e0; object-fit: cover;}
528
- .sync-buttons { display: flex; gap: 10px; margin-bottom: 20px; flex-wrap: wrap; }
529
- .message { padding: 10px 15px; border-radius: 6px; margin-bottom: 15px; font-size: 0.9rem;}
530
- .message.success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb;}
531
- .message.error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb;}
532
- .message.warning { background-color: #fff3cd; color: #856404; border: 1px solid #ffeeba; }
533
- </style>
534
- </head>
535
- <body>
536
- <div class="container">
537
- <div class="header">
538
- <h1><i class="fas fa-tools"></i> Админ-панель "Раина"</h1>
539
- <a href="{{ url_for('landing') }}" class="button"><i class="fas fa-home"></i> Перейти на сайт</a>
540
- </div>
541
-
542
- {% with messages = get_flashed_messages(with_categories=true) %}
543
- {% if messages %}{% for category, message in messages %}<div class="message {{ category }}">{{ message }}</div>{% endfor %}{% endif %}
544
- {% endwith %}
545
 
546
- <div class="section">
547
- <h2><i class="fas fa-sync-alt"></i> Синхронизация</h2>
548
- <div class="sync-buttons">
549
- <form method="POST" action="{{ url_for('force_upload') }}" style="display: inline;" onsubmit="return confirm('Загрузить локальные данные на сервер?');">
550
- <button type="submit" class="button"><i class="fas fa-upload"></i> Загрузить БД</button>
551
- </form>
552
- <form method="POST" action="{{ url_for('force_download') }}" style="display: inline;" onsubmit="return confirm('Скачать данные с сервера (перезапишет локальные)?');">
553
- <button type="submit" class="button"><i class="fas fa-download"></i> Скачать БД</button>
554
- </form>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
555
  </div>
 
556
  </div>
 
557
 
558
- <div class="section">
559
- <h2><i class="fas fa-concierge-bell"></i> Услуги "под ключ"</h2>
560
- <details><summary><i class="fas fa-plus-circle"></i> Добавить услугу</summary><div class="form-content">
561
- <form method="POST" enctype="multipart/form-data">
562
- <input type="hidden" name="action" value="add_service"><label for="add_service_title">Заголовок *:</label><input type="text" id="add_service_title" name="title" required><label for="add_service_icon">Иконка (FontAwesome) *:</label><input type="text" id="add_service_icon" name="icon" placeholder="fas fa-tools" required><label for="add_service_description">Описание *:</label><textarea id="add_service_description" name="description" rows="3" required></textarea><label for="add_service_photo">Фото:</label><input type="file" id="add_service_photo" name="photo" accept="image/*"><button type="submit"><i class="fas fa-save"></i> Добавить</button>
563
- </form>
564
- </div></details>
565
- <h3>Список услуг:</h3>
566
- {% if services %}<div class="item-list">{% for service in services %}<div class="item">
567
- <div style="display: flex; gap: 15px; align-items: flex-start;"><div class="photo-preview">{% if service.photo %}<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/services/{{ service.photo }}" alt="Фото">{% endif %}</div><div style="flex-grow: 1;"><h3 style="margin: 0 0 5px;"><i class="{{ service.icon }} fa-fw"></i> {{ service.title }}</h3><p>{{ service.description }}</p></div></div>
568
- <div class="item-actions"><button type="button" onclick="toggleEditForm('edit-service-{{ loop.index0 }}')"><i class="fas fa-edit"></i> Редактировать</button><form method="POST" style="margin:0;" onsubmit="return confirm('Удалить услугу \'{{ service.title }}\'?');"><input type="hidden" name="action" value="delete_service"><input type="hidden" name="index" value="{{ loop.index0 }}"><button type="submit" class="delete-button"><i class="fas fa-trash-alt"></i> Удалить</button></form></div>
569
- <div id="edit-service-{{ loop.index0 }}" class="edit-form-container"><form method="POST" enctype="multipart/form-data"><input type="hidden" name="action" value="edit_service"><input type="hidden" name="index" value="{{ loop.index0 }}"><label>Заголовок *:</label><input type="text" name="title" value="{{ service.title }}" required><label>Иконка *:</label><input type="text" name="icon" value="{{ service.icon }}" required><label>Описание *:</label><textarea name="description" rows="3" required>{{ service.description }}</textarea><label>Заменить фото:</label><input type="file" name="photo" accept="image/*"><button type="submit"><i class="fas fa-save"></i> Сохранить</button></form></div>
570
- </div>{% endfor %}</div>{% else %}<p>Услуг пока нет.</p>{% endif %}
571
- </div>
572
-
573
- <div class="section">
574
- <h2><i class="fas fa-building"></i> Реализованные проекты</h2>
575
- <details><summary><i class="fas fa-plus-circle"></i> Добавить проект</summary><div class="form-content">
576
- <form method="POST" enctype="multipart/form-data">
577
- <input type="hidden" name="action" value="add_project"><label for="add_project_title">Название проекта *:</label><input type="text" id="add_project_title" name="title" required><label for="add_project_description">Краткое описание *:</label><textarea id="add_project_description" name="description" rows="2" required></textarea><label for="add_project_photo">Фото *:</label><input type="file" id="add_project_photo" name="photo" accept="image/*" required><button type="submit"><i class="fas fa-save"></i> Добавить</button>
578
- </form>
579
- </div></details>
580
- <h3>Список проектов:</h3>
581
- {% if projects %}<div class="item-list">{% for project in projects %}<div class="item">
582
- <div style="display: flex; gap: 15px; align-items: flex-start;"><div class="photo-preview">{% if project.photo %}<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/projects/{{ project.photo }}" alt="Фото">{% endif %}</div><div style="flex-grow: 1;"><h3 style="margin: 0 0 5px;">{{ project.title }}</h3><p>{{ project.description }}</p></div></div>
583
- <div class="item-actions"><button type="button" onclick="toggleEditForm('edit-project-{{ loop.index0 }}')"><i class="fas fa-edit"></i> Редактировать</button><form method="POST" style="margin:0;" onsubmit="return confirm('Удалить проект \'{{ project.title }}\'?');"><input type="hidden" name="action" value="delete_project"><input type="hidden" name="index" value="{{ loop.index0 }}"><button type="submit" class="delete-button"><i class="fas fa-trash-alt"></i> Удалить</button></form></div>
584
- <div id="edit-project-{{ loop.index0 }}" class="edit-form-container"><form method="POST" enctype="multipart/form-data"><input type="hidden" name="action" value="edit_project"><input type="hidden" name="index" value="{{ loop.index0 }}"><label>Название *:</label><input type="text" name="title" value="{{ project.title }}" required><label>Описание *:</label><textarea name="description" rows="2" required>{{ project.description }}</textarea><label>Заменить фото:</label><input type="file" name="photo" accept="image/*"><button type="submit"><i class="fas fa-save"></i> Сохранить</button></form></div>
585
- </div>{% endfor %}</div>{% else %}<p>Проектов пока нет.</p>{% endif %}
 
 
586
  </div>
 
587
 
588
- <div class="section">
589
- <h2><i class="fas fa-tags"></i> Категории оборудования</h2>
590
- <details><summary><i class="fas fa-plus-circle"></i> Добавить категорию</summary><div class="form-content">
591
- <form method="POST"><input type="hidden" name="action" value="add_category"><label for="add_category_name">Название:</label><input type="text" id="add_category_name" name="category_name" required><button type="submit"><i class="fas fa-plus"></i> Добавить</button></form>
592
- </div></details>
593
- {% if categories %}<div class="item-list">{% for category in categories %}<div class="item" style="display: flex; justify-content: space-between; align-items: center;"><span>{{ category }}</span><form method="POST" style="margin: 0;" onsubmit="return confirm('Удалить категорию \'{{ category }}\'?');"><input type="hidden" name="action" value="delete_category"><input type="hidden" name="category_name" value="{{ category }}"><button type="submit" class="delete-button" style="padding: 5px 10px; font-size: 0.8rem; margin: 0;"><i class="fas fa-trash-alt"></i></button></form></div>{% endfor %}</div>{% else %}<p>Категорий пока нет.</p>{% endif %}
 
 
 
 
 
 
594
  </div>
595
 
596
- <div class="section">
597
- <h2><i class="fas fa-box-open"></i> Оборудование</h2>
598
- <details><summary><i class="fas fa-plus-circle"></i> Добавить оборудование</summary><div class="form-content">
599
- <form method="POST" enctype="multipart/form-data"><input type="hidden" name="action" value="add_product"><label for="add_name">Название *:</label><input type="text" id="add_name" name="name" required><label for="add_price">Цена (KGS) *:</label><input type="number" id="add_price" name="price" step="0.01" min="0" required><label for="add_category">Категория:</label><select id="add_category" name="category"><option value="Без категории">Без категории</option>{% for category in categories %}<option value="{{ category }}">{{ category }}</option>{% endfor %}</select><label for="add_photos">Фото:</label><input type="file" id="add_photos" name="photos" accept="image/*" multiple><button type="submit"><i class="fas fa-save"></i> Добавить</button></form>
600
- </div></details>
601
- <h3>Список оборудования:</h3>
602
- {% if products %}<div class="item-list">{% for product in products %}<div class="item">
603
- <div style="display: flex; gap: 15px; align-items: flex-start;"><div class="photo-preview">{% if product.get('photos') %}<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product['photos'][0] }}" alt="Фото">{% else %}<img src="https://via.placeholder.com/70x70.png?text=N/A">{% endif %}</div><div style="flex-grow: 1;"><h3 style="margin:0 0 5px;">{{ product['name'] }}</h3><p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p><p><strong>Цена:</strong> {{ "%.2f"|format(product['price']) }} KGS</p></div></div>
604
- <div class="item-actions"><button type="button" onclick="toggleEditForm('edit-form-{{ loop.index0 }}')"><i class="fas fa-edit"></i> Редактировать</button><form method="POST" style="margin:0;" onsubmit="return confirm('Удалить \'{{ product['name'] }}\'?');"><input type="hidden" name="action" value="delete_product"><input type="hidden" name="index" value="{{ loop.index0 }}"><button type="submit" class="delete-button"><i class="fas fa-trash-alt"></i> Удалить</button></form></div>
605
- <div id="edit-form-{{ loop.index0 }}" class="edit-form-container"><form method="POST" enctype="multipart/form-data"><input type="hidden" name="action" value="edit_product"><input type="hidden" name="index" value="{{ loop.index0 }}"><label>Название *:</label><input type="text" name="name" value="{{ product['name'] }}" required><label>Цена *:</label><input type="number" name="price" step="0.01" min="0" value="{{ product['price'] }}" required><label>Категория:</label><select name="category"><option value="Без категории">Без категории</option>{% for category in categories %}<option value="{{ category }}" {% if product.get('category') == category %}selected{% endif %}>{{ category }}</option>{% endfor %}</select><label>Заменить фото:</label><input type="file" name="photos" accept="image/*" multiple><button type="submit"><i class="fas fa-save"></i> Сохранить</button></form></div>
606
- </div>{% endfor %}</div>{% else %}<p>Оборудования пока нет.</p>{% endif %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
607
  </div>
608
  </div>
609
- <script>
610
- function toggleEditForm(formId) {
611
- const form = document.getElementById(formId);
612
- if (form) form.style.display = form.style.display === 'none' || form.style.display === '' ? 'block' : 'none';
613
- }
614
- </script>
615
  </body>
616
  </html>
617
  '''
@@ -622,7 +560,7 @@ def landing():
622
  return render_template_string(
623
  LANDING_TEMPLATE,
624
  services=data.get('services', []),
625
- products=data.get('products', []),
626
  categories=sorted(data.get('categories', [])),
627
  projects=data.get('projects', []),
628
  repo_id=REPO_ID,
@@ -634,163 +572,162 @@ def landing():
634
  @app.route('/admin', methods=['GET', 'POST'])
635
  def admin():
636
  data = load_data()
637
-
638
  if request.method == 'POST':
639
  action = request.form.get('action')
640
  logging.info(f"Admin action: {action}")
641
  try:
642
  if action == 'add_category':
643
- category_name = request.form.get('category_name', '').strip()
644
- if category_name and category_name not in data['categories']:
645
- data['categories'].append(category_name)
646
- save_data(data)
647
- flash(f"Категория '{category_name}' добавлена.", 'success')
648
- else:
649
- flash("Категория уже существует или пуста.", 'error')
650
-
651
  elif action == 'delete_category':
652
- category_to_delete = request.form.get('category_name')
653
- if category_to_delete in data['categories']:
654
- data['categories'].remove(category_to_delete)
655
- for p in data['products']:
656
- if p.get('category') == category_to_delete: p['category'] = 'Без категории'
657
- save_data(data)
658
- flash(f"Категория '{category_to_delete}' удалена.", 'success')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
659
 
660
- elif action == 'add_service':
661
- title, icon, desc = request.form.get('title', ''), request.form.get('icon', ''), request.form.get('description', '')
662
- if all((title, icon, desc)):
663
- photo_list = upload_photos_to_hf(request.files.getlist('photo'), title, 'services')
664
- data['services'].append({'title': title, 'icon': icon, 'description': desc, 'photo': photo_list[0] if photo_list else None})
665
- save_data(data)
 
 
666
  flash(f"Услуга '{title}' добавлена.", 'success')
667
- else:
668
- flash("Все поля услуги обязательны.", 'error')
669
-
670
- elif action == 'edit_service':
671
- idx = int(request.form.get('index'))
672
- service = data['services'][idx]
673
- service['title'], service['icon'], service['description'] = request.form.get('title'), request.form.get('icon'), request.form.get('description')
674
- if request.files.get('photo'):
675
- new_photos = upload_photos_to_hf(request.files.getlist('photo'), service['title'], 'services')
676
- if new_photos:
677
- delete_photos_from_hf([service.get('photo')], 'services')
678
- service['photo'] = new_photos[0]
679
- save_data(data)
680
- flash(f"Услуга '{service['title']}' обновлена.", 'success')
681
-
682
  elif action == 'delete_service':
683
- idx = int(request.form.get('index'))
684
- deleted = data['services'].pop(idx)
685
- delete_photos_from_hf([deleted.get('photo')], 'services')
686
- save_data(data)
687
- flash(f"Услуга '{deleted.get('title')}' удалена.", 'success')
688
-
689
- elif action == 'add_project':
690
- title, desc = request.form.get('title', ''), request.form.get('description', '')
691
- if all((title, desc)) and request.files.get('photo'):
692
- photo_list = upload_photos_to_hf(request.files.getlist('photo'), title, 'projects')
693
- data['projects'].append({'title': title, 'description': desc, 'photo': photo_list[0] if photo_list else None})
694
- save_data(data)
695
- flash(f"Проект '{title}' добавлен.", 'success')
696
- else:
697
- flash("Название, описание и фото проекта обязательны.", 'error')
698
-
699
- elif action == 'edit_project':
700
- idx = int(request.form.get('index'))
701
- project = data['projects'][idx]
702
- project['title'], project['description'] = request.form.get('title'), request.form.get('description')
703
- if request.files.get('photo'):
704
- new_photos = upload_photos_to_hf(request.files.getlist('photo'), project['title'], 'projects')
705
- if new_photos:
706
- delete_photos_from_hf([project.get('photo')], 'projects')
707
- project['photo'] = new_photos[0]
708
- save_data(data)
709
- flash(f"Проект '{project['title']}' обновлен.", 'success')
710
 
711
  elif action == 'delete_project':
712
- idx = int(request.form.get('index'))
713
- deleted = data['projects'].pop(idx)
714
- delete_photos_from_hf([deleted.get('photo')], 'projects')
715
- save_data(data)
716
- flash(f"Проект '{deleted.get('title')}' удален.", 'success')
717
-
718
- elif action == 'add_product':
719
- name, price_str, category = request.form.get('name', ''), request.form.get('price', ''), request.form.get('category')
720
- if all((name, price_str)):
721
- photos = upload_photos_to_hf(request.files.getlist('photos'), name, 'photos')
722
- data['products'].append({'name': name, 'price': float(price_str), 'category': category, 'photos': photos})
723
- save_data(data)
724
- flash(f"Оборудование '{name}' добавлено.", 'success')
725
- else:
726
- flash("Название и цена обязательны.", 'error')
727
-
728
- elif action == 'edit_product':
729
- idx = int(request.form.get('index'))
730
- product = data['products'][idx]
731
- product['name'], product['price'], product['category'] = request.form.get('name'), float(request.form.get('price')), request.form.get('category')
732
- if request.files.getlist('photos'):
733
- new_photos = upload_photos_to_hf(request.files.getlist('photos'), product['name'], 'photos')
734
- if new_photos:
735
- delete_photos_from_hf(product.get('photos', []), 'photos')
736
- product['photos'] = new_photos
737
- save_data(data)
738
- flash(f"Оборудование '{product['name']}' обновлено.", 'success')
739
-
740
- elif action == 'delete_product':
741
- idx = int(request.form.get('index'))
742
- deleted = data['products'].pop(idx)
743
- delete_photos_from_hf(deleted.get('photos', []), 'photos')
744
- save_data(data)
745
- flash(f"Оборудование '{deleted.get('name')}' удалено.", 'success')
746
 
 
747
  return redirect(url_for('admin'))
748
  except Exception as e:
749
  logging.error(f"Admin action '{action}' failed: {e}", exc_info=True)
750
- flash(f"Ошибка при выполнении действия: {e}", 'error')
751
  return redirect(url_for('admin'))
752
 
753
  return render_template_string(
754
  ADMIN_TEMPLATE,
755
- products=data.get('products', []),
756
  categories=sorted(data.get('categories', [])),
757
  services=data.get('services', []),
758
- projects=data.get('projects', []),
759
- repo_id=REPO_ID
760
  )
761
 
762
- def upload_photos_to_hf(photo_files, item_name, folder):
763
- if not photo_files or not HF_TOKEN_WRITE:
764
- if any(f and f.filename for f in photo_files): flash("HF_TOKEN (write) не настроен, фото не загружены.", "warning")
765
- return []
766
- api, uploaded_photos, temp_dir = HfApi(), [], 'uploads_temp'
767
- os.makedirs(temp_dir, exist_ok=True)
768
- for photo in photo_files:
769
- if photo and photo.filename:
770
- try:
771
- ext = os.path.splitext(photo.filename)[1].lower()
772
- if ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']: continue
773
- safe_name = secure_filename(item_name.replace(' ', '_'))[:50]
774
- filename = f"{safe_name}_{uuid.uuid4().hex[:8]}{ext}"
775
- temp_path = os.path.join(temp_dir, filename)
776
- photo.save(temp_path)
777
- api.upload_file(path_or_fileobj=temp_path, path_in_repo=f"{folder}/{filename}", repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE)
778
- uploaded_photos.append(filename)
779
- os.remove(temp_path)
780
- except Exception as e:
781
- logging.error(f"Upload failed for {photo.filename}: {e}", exc_info=True)
782
- return uploaded_photos
783
 
784
- def delete_photos_from_hf(photo_list, folder):
785
- if not photo_list or not HF_TOKEN_WRITE: return
786
- to_delete = [p for p in photo_list if p]
787
- if not to_delete: return
788
  try:
789
- HfApi().delete_files(repo_id=REPO_ID, paths_in_repo=[f"{folder}/{p}" for p in to_delete], repo_type="dataset", token=HF_TOKEN_WRITE)
790
- logging.info(f"Deleted photos from HF: {to_delete}")
 
 
 
 
 
 
 
791
  except Exception as e:
792
- logging.error(f"Failed to delete photos from HF: {e}", exc_info=True)
793
- flash("Не удалось удалить старые фото с сервера.", "warning")
794
 
795
  @app.route('/force_upload', methods=['POST'])
796
  def force_upload():
@@ -800,14 +737,12 @@ def force_upload():
800
 
801
  @app.route('/force_download', methods=['POST'])
802
  def force_download():
803
- if download_db_from_hf():
804
- flash("Данные скачаны с сервера.", 'success')
805
- else:
806
- flash("Не удалось скачать данные.", 'error')
807
  return redirect(url_for('admin'))
808
 
809
  if __name__ == '__main__':
810
- logging.info("Application starting...")
811
  download_db_from_hf()
812
  if HF_TOKEN_WRITE:
813
  threading.Thread(target=periodic_backup, daemon=True).start()
 
1
+ from flask import Flask, render_template_string, request, redirect, url_for, flash
2
  import json
3
  import os
4
  import logging
 
10
  from werkzeug.utils import secure_filename
11
  from dotenv import load_dotenv
12
  import requests
 
13
 
14
  load_dotenv()
15
 
16
  app = Flask(__name__)
17
+ app.secret_key = 'raina_hvac_secret_key_v2_projects_dynamic'
18
  DATA_FILE = 'data.json'
19
 
20
  SYNC_FILES = [DATA_FILE]
 
43
  for attempt in range(retries + 1):
44
  try:
45
  logging.info(f"Downloading {file_name} (Attempt {attempt + 1}/{retries + 1})...")
46
+ hf_hub_download(
47
+ repo_id=REPO_ID, filename=file_name, repo_type="dataset", token=token_to_use,
48
+ local_dir=".", local_dir_use_symlinks=False, force_download=True, resume_download=False
 
 
 
 
 
 
49
  )
50
+ logging.info(f"Successfully downloaded {file_name}.")
51
  success = True
52
  break
53
  except RepositoryNotFoundError:
54
+ logging.error(f"Repository {REPO_ID} not found. Download cancelled.")
55
  return False
56
  except HfHubHTTPError as e:
57
  if e.response.status_code == 404:
58
+ logging.warning(f"File {file_name} not found in repo {REPO_ID}. Creating empty local file.")
59
+ if not os.path.exists(file_name):
60
  try:
61
+ with open(file_name, 'w', encoding='utf-8') as f:
62
+ json.dump({'equipment': [], 'categories': [], 'services': [], 'projects': []}, f)
 
 
63
  except Exception as create_e:
64
  logging.error(f"Failed to create empty local file {file_name}: {create_e}")
65
+ success = True
66
  break
67
  else:
68
+ logging.error(f"HTTP error downloading {file_name}: {e}. Retrying...")
 
 
69
  except Exception as e:
70
+ logging.error(f"Unexpected error downloading {file_name}: {e}. Retrying...", exc_info=True)
71
  if attempt < retries:
72
  time.sleep(delay)
73
  if not success:
74
  logging.error(f"Failed to download {file_name} after {retries + 1} attempts.")
75
  all_successful = False
 
76
  return all_successful
77
 
78
  def upload_db_to_hf(specific_file=None):
79
  if not HF_TOKEN_WRITE:
80
+ logging.warning("HF_TOKEN (for writing) not set. Skipping upload.")
81
  return
82
  try:
83
  api = HfApi()
84
  files_to_upload = [specific_file] if specific_file else SYNC_FILES
85
+ logging.info(f"Starting upload of {files_to_upload} to {REPO_ID}...")
86
  for file_name in files_to_upload:
87
  if os.path.exists(file_name):
88
+ api.upload_file(
89
+ path_or_fileobj=file_name, path_in_repo=file_name, repo_id=REPO_ID,
90
+ repo_type="dataset", token=HF_TOKEN_WRITE,
91
+ commit_message=f"Sync {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
92
+ )
93
+ logging.info(f"File {file_name} successfully uploaded.")
 
 
 
 
 
 
94
  else:
95
  logging.warning(f"File {file_name} not found locally, skipping upload.")
 
96
  except Exception as e:
97
+ logging.error(f"Error during Hugging Face upload: {e}", exc_info=True)
98
 
99
  def periodic_backup():
100
  backup_interval = 1800
 
101
  while True:
102
  time.sleep(backup_interval)
103
  logging.info("Starting periodic backup...")
 
105
  logging.info("Periodic backup finished.")
106
 
107
  def load_data():
108
+ default_data = {'equipment': [], 'categories': [], 'services': [], 'projects': []}
109
  try:
110
  with open(DATA_FILE, 'r', encoding='utf-8') as file:
111
  data = json.load(file)
 
112
  if not isinstance(data, dict):
113
+ raise ValueError("Data is not a dictionary")
114
+ if 'equipment' not in data: data['equipment'] = []
115
  if 'categories' not in data: data['categories'] = []
116
  if 'services' not in data: data['services'] = []
117
  if 'projects' not in data: data['projects'] = []
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
+ return load_data()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  return default_data
124
 
125
  def save_data(data):
126
  try:
127
  if not isinstance(data, dict):
128
+ logging.error("Attempted to save invalid data structure. Aborting.")
129
  return
 
 
 
 
130
  with open(DATA_FILE, 'w', encoding='utf-8') as file:
131
  json.dump(data, file, ensure_ascii=False, indent=4)
132
+ logging.info(f"Data saved to {DATA_FILE}")
133
  upload_db_to_hf(specific_file=DATA_FILE)
134
  except Exception as e:
135
+ logging.error(f"Error saving data: {e}", exc_info=True)
136
 
137
  LANDING_TEMPLATE = '''
138
  <!DOCTYPE html>
 
146
  <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap" rel="stylesheet">
147
  <style>
148
  :root {
149
+ --dark-bg: #12121c; --card-bg: #1a1a2e; --primary-color: #a955ff;
150
+ --secondary-color: #6a0dad; --text-color: #e0e0e0; --text-muted: #a0a0b0;
 
 
 
 
151
  --accent-glow: rgba(169, 85, 255, 0.3);
152
  }
153
  * { margin: 0; padding: 0; box-sizing: border-box; scroll-behavior: smooth; }
154
  body { font-family: 'Montserrat', sans-serif; background-color: var(--dark-bg); color: var(--text-color); line-height: 1.7; font-size: 16px; }
155
  .container { max-width: 1200px; margin: 0 auto; padding: 0 20px; }
156
+ section { padding: clamp(3.5rem, 8vw, 5rem) 0; overflow: hidden; }
157
  h1, h2, h3 { font-weight: 700; color: #fff; line-height: 1.3; }
158
+ h1 { font-size: clamp(2.2rem, 6vw, 4rem); }
159
  h2 { font-size: clamp(2rem, 5vw, 3rem); text-align: center; margin-bottom: 60px; position: relative; }
160
  h2::after { content: ''; display: block; width: 80px; height: 4px; background: linear-gradient(90deg, var(--primary-color), var(--secondary-color)); margin: 15px auto 0; border-radius: 2px; }
161
  h3 { font-size: clamp(1.2rem, 3vw, 1.5rem); color: var(--primary-color); margin-bottom: 15px; }
162
  p { margin-bottom: 1rem; color: var(--text-muted); }
163
  .btn { display: inline-block; padding: 12px 28px; background: linear-gradient(90deg, var(--primary-color), var(--secondary-color)); color: #fff; border-radius: 50px; text-decoration: none; font-weight: 600; transition: all 0.3s ease; box-shadow: 0 4px 15px var(--accent-glow); }
164
  .btn:hover { transform: translateY(-3px) scale(1.05); box-shadow: 0 8px 25px var(--accent-glow); }
165
+ .header { position: fixed; top: 0; left: 0; width: 100%; z-index: 1000; padding: 15px 0; background-color: rgba(18, 18, 28, 0.85); backdrop-filter: blur(10px); transition: all 0.3s ease; }
 
166
  .header.scrolled { padding: 10px 0; box-shadow: 0 2px 10px rgba(0,0,0,0.3); }
167
  .navbar { display: flex; justify-content: space-between; align-items: center; }
168
+ .logo { font-size: clamp(1.5rem, 4vw, 1.8rem); font-weight: 700; color: #fff; text-decoration: none; }
169
+ .nav-links { display: flex; gap: 30px; list-style: none; }
170
+ .nav-links a { color: var(--text-color); text-decoration: none; font-weight: 600; transition: color 0.3s ease; }
171
+ .nav-links a:hover { color: var(--primary-color); }
172
+ .menu-toggle { display: none; font-size: 1.5rem; cursor: pointer; border: none; background: none; color: white; }
173
+ #hero { min-height: 100vh; display: flex; align-items: center; background-image: linear-gradient(rgba(18, 18, 28, 0.7), rgba(18, 18, 28, 1)), url(https://images.unsplash.com/photo-1558221639-2c7158995165?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1740&q=80); background-size: cover; background-position: center; }
174
  .hero-content { text-align: center; max-width: 800px; margin: 0 auto; }
175
  .hero-content p { font-size: clamp(1rem, 2.5vw, 1.2rem); margin: 30px 0; max-width: 600px; margin-left: auto; margin-right: auto;}
176
+ .grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 60px; align-items: center; }
177
+ .about-img { width: 100%; border-radius: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.4); }
178
+ .services-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 30px; }
 
 
179
  .service-card, .turnkey-card { background-color: var(--card-bg); padding: 30px; border-radius: 15px; border: 1px solid #2a2a4a; transition: all 0.3s ease; }
180
  .service-card:hover, .turnkey-card:hover { transform: translateY(-5px); border-color: var(--primary-color); box-shadow: 0 8px 25px var(--accent-glow); }
181
  .service-card i { font-size: 2.5rem; color: var(--primary-color); margin-bottom: 20px; }
182
+ .turnkey-card { padding: 0; display: flex; flex-direction: column; }
183
+ .turnkey-img { width: 100%; height: 200px; object-fit: cover; border-radius: 15px 15px 0 0; }
 
184
  .turnkey-content { padding: 30px; flex-grow: 1;}
185
+ .equipment-filters { display: flex; justify-content: center; flex-wrap: wrap; gap: 15px; margin-bottom: 40px; }
186
+ .filter-btn { padding: 8px 20px; border: 1px solid var(--primary-color); background-color: transparent; color: var(--primary-color); border-radius: 20px; cursor: pointer; transition: all 0.3s; }
 
187
  .filter-btn.active, .filter-btn:hover { background-color: var(--primary-color); color: #fff; }
188
+ .equipment-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 30px; }
189
  .equipment-card { background-color: var(--card-bg); border-radius: 15px; overflow: hidden; text-align: center; padding: 20px; border: 1px solid #2a2a4a; transition: all 0.3s ease; }
190
  .equipment-card img { width: 100%; height: 180px; object-fit: contain; margin-bottom: 15px; }
191
  .equipment-card h3 { font-size: 1.2rem; }
192
  .equipment-card .price { font-size: 1.3rem; font-weight: 700; color: #fff; margin: 10px 0; }
193
+ .projects-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 30px; }
194
+ .project-card { position: relative; border-radius: 15px; overflow: hidden; min-height: 400px; }
195
  .project-card img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform 0.4s ease; }
196
+ .project-overlay { position: absolute; bottom: 0; left: 0; right: 0; background: linear-gradient(to top, rgba(18,18,28,1) 0%, rgba(18,18,28,0) 100%); padding: 40px 20px 20px; }
197
+ .project-card h3 { margin-bottom: 5px; font-size: 1.3rem; }
198
+ .project-card p { margin-bottom: 0; transition: opacity 0.4s ease; opacity: 0; max-height: 0; overflow: hidden; }
199
  .project-card:hover img { transform: scale(1.05); }
200
  .project-card:hover p { opacity: 1; max-height: 200px; }
 
201
  #contact { background-color: var(--card-bg); }
202
  .contact-content { text-align: center; }
203
  .contact-info { margin-top: 40px; display: flex; flex-direction: column; align-items: center; gap: 20px; }
204
  .contact-info p { font-size: 1.2rem; margin-bottom: 0; }
205
  .contact-info a { color: var(--primary-color); text-decoration: none; font-weight: 600; }
206
  .footer { text-align: center; padding: 30px 0; background-color: #0d0d14; }
207
+ @media (max-width: 992px) {
208
+ .grid-2 { grid-template-columns: 1fr; text-align: center; }
209
+ .about-img { margin-bottom: 30px; max-width: 500px; margin-left: auto; margin-right: auto;}
 
 
 
 
 
 
 
 
210
  }
211
+ @media (max-width: 768px) {
212
+ .nav-links { position: fixed; top: 0; right: -100%; width: min(75vw, 300px); height: 100vh; background-color: var(--card-bg); flex-direction: column; justify-content: center; align-items: center; transition: right 0.4s ease-in-out; box-shadow: -5px 0 15px rgba(0,0,0,0.2); }
213
+ .nav-links.active { right: 0; }
214
+ .menu-toggle { display: block; z-index: 1001; }
215
+ h2 { margin-bottom: 40px; }
216
+ .projects-grid { grid-template-columns: 1fr; }
 
 
 
 
 
 
 
 
 
 
217
  }
 
 
 
 
218
  </style>
219
  </head>
220
  <body>
221
  <header class="header">
222
  <div class="container navbar">
223
  <a href="#" class="logo">Раина</a>
224
+ <ul class="nav-links">
225
+ <li><a href="#about">О компании</a></li>
226
+ <li><a href="#services">Услуги</a></li>
227
+ <li><a href="#turnkey">Под ключ</a></li>
228
+ <li><a href="#equipment">Оборудование</a></li>
229
+ <li><a href="#projects">Проекты</a></li>
230
+ <li><a href="#contact">Контакты</a></li>
231
+ </ul>
232
+ <button class="menu-toggle" aria-label="Открыть меню"><i class="fas fa-bars"></i></button>
233
  </div>
234
  </header>
235
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  <section id="hero">
237
  <div class="container hero-content">
238
  <h1>ОсОО "Раина": Ваш Партнер в Вентиляции и Кондиционировании</h1>
 
245
  <div class="container">
246
  <h2>О Нашей Компании</h2>
247
  <div class="grid-2">
248
+ <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">
249
  <div>
250
  <h3>Основание и История</h3>
251
  <p>Компания "Раина" была основана в 2009 году. За эти годы мы зарекомендовали себя как надежный партнер, стремящийся к инновациям и совершенству в области климатических решений.</p>
252
  <h3>Наша Миссия</h3>
253
  <p>Наша миссия — создание оптимального микроклимата для наших клиентов, обеспечивающего комфорт, здоровье и высокую производительность.</p>
254
  <h3>Профессиональная Команда</h3>
255
+ <p>Наша команда состоит из высококвалифицированных инженеров и техников, обладающих глубокими знаниями и опытом в области HVAC.</p>
256
  </div>
257
  </div>
258
  </div>
 
264
  <div class="services-grid">
265
  <div class="service-card"><i class="fas fa-drafting-compass"></i><h3>Проектирование</h3><p>Точные расчеты, 3D-модели и вся необходимая проектная документация.</p></div>
266
  <div class="service-card"><i class="fas fa-tools"></i><h3>Монтаж</h3><p>Профессиональная установка всех типов систем HVAC, от бытовых до промышленных.</p></div>
267
+ <div class="service-card"><i class="fas fa-headset"></i><h3>Сервис 24/7</h3><p>Плановое обслуживание и оперативный аварийный ремонт в любое время.</p></div>
268
  <div class="service-card"><i class="fas fa-sync-alt"></i><h3>Модернизация</h3><p>Повышение энергоэффективности и снижение расходов на эксплуатацию.</p></div>
269
  </div>
270
  </div>
271
  </section>
272
 
273
+ <section id="turnkey" style="background-color: var(--card-bg);">
274
  <div class="container">
275
  <h2>Услуги "под ключ"</h2>
276
  {% if services %}
277
  <div class="services-grid">
278
  {% for service in services %}
279
  <div class="turnkey-card">
280
+ {% if service.photo %}
281
+ <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/services/{{ service.photo }}" alt="{{ service.title }}" class="turnkey-img">
282
+ {% endif %}
283
  <div class="turnkey-content">
284
  <h3><i class="{{ service.icon }} fa-fw" style="margin-right: 8px; color: var(--primary-color);"></i>{{ service.title }}</h3>
285
  <p>{{ service.description }}</p>
 
288
  {% endfor %}
289
  </div>
290
  {% else %}
291
+ <p style="text-align: center;">Информация об услугах "под ключ" скоро появится на сайте.</p>
292
  {% endif %}
293
  </div>
294
  </section>
295
 
296
+ <section id="equipment">
297
  <div class="container">
298
  <h2>Наше Оборудование</h2>
299
+ {% if equipment %}
300
  <div class="equipment-filters">
301
  <button class="filter-btn active" data-filter="all">Все</button>
302
+ {% for category in categories %}
303
+ <button class="filter-btn" data-filter="{{ category }}">{{ category }}</button>
304
+ {% endfor %}
305
  </div>
306
  <div class="equipment-grid">
307
+ {% for item in equipment %}
308
+ <div class="equipment-card" data-category="{{ item.get('category', 'all') }}">
309
+ {% if item.photo %}
310
+ <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/equipment/{{ item.photo }}" alt="{{ item.name }}">
311
+ {% else %}
312
+ <img src="https://via.placeholder.com/250x180.png?text=No+Image" alt="No Image">
313
+ {% endif %}
314
+ <h3>{{ item.name }}</h3>
315
+ <p class="price">{{ "%.2f"|format(item.price) }} KGS</p>
316
+ <a href="https://api.whatsapp.com/send?phone={{ whatsapp_phone }}&text=Здравствуйте, интересует оборудование: {{ item.name }}" target="_blank" class="btn" style="padding: 8px 20px; font-size: 0.9rem;">Запросить</a>
317
  </div>
318
  {% endfor %}
319
  </div>
 
322
  {% endif %}
323
  </div>
324
  </section>
325
+
326
  <section id="projects">
327
  <div class="container">
328
  <h2>Реализованные Проекты</h2>
 
330
  <div class="projects-grid">
331
  {% for project in projects %}
332
  <div class="project-card">
333
+ <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/projects/{{ project.photo }}" alt="{{ project.title }}">
334
  <div class="project-overlay">
335
  <h3>{{ project.title }}</h3>
336
  <p>{{ project.description }}</p>
 
339
  {% endfor %}
340
  </div>
341
  {% else %}
342
+ <p style="text-align: center;">Информация о реализованных проектах скоро появится на сайте.</p>
343
  {% endif %}
344
  </div>
345
  </section>
346
 
347
  <section id="contact">
348
  <div class="container contact-content">
349
+ <h2>Контакты</h2>
350
+ <p>Готовы стать вашим надежным партнером в создании идеального климата.</p>
351
  <div class="contact-info">
352
  <p><strong>Свяжитесь с нами:</strong> <a href="tel:{{ contact_phone }}">{{ contact_phone }}</a></p>
353
  <a href="https://api.whatsapp.com/send?phone={{ whatsapp_phone }}&text=Здравствуйте, я хотел(а) бы получить консультацию по вашим услугам." target="_blank" class="btn"><i class="fab fa-whatsapp"></i> Написать в WhatsApp</a>
354
  </div>
355
  <div style="margin-top: 40px; font-size: 0.9rem; color: var(--text-muted);">
356
  <p><strong>Реквизиты:</strong> ОсОО «Раина», ИНН: 00812202110194, ОКПО: 31290279</p>
 
357
  </div>
358
  </div>
359
  </section>
360
 
361
  <footer class="footer">
362
+ <p>© {{ now.year }} ОсОО "Раина". Все права защищены.</p>
 
 
363
  </footer>
364
 
365
  <script>
366
  document.addEventListener('DOMContentLoaded', function() {
367
  const header = document.querySelector('.header');
368
  const menuToggle = document.querySelector('.menu-toggle');
369
+ const navLinks = document.querySelector('.nav-links');
370
+ window.addEventListener('scroll', () => { header.classList.toggle('scrolled', window.scrollY > 50); });
371
+ menuToggle.addEventListener('click', () => { navLinks.classList.toggle('active'); });
372
+ document.querySelectorAll('.nav-links a').forEach(link => {
373
+ link.addEventListener('click', () => { navLinks.classList.remove('active'); });
 
 
 
 
 
 
 
 
 
374
  });
 
375
  const filterContainer = document.querySelector('.equipment-filters');
376
  if (filterContainer) {
377
  filterContainer.addEventListener('click', (e) => {
378
+ if (!e.target.matches('.filter-btn')) return;
379
+ filterContainer.querySelector('.active').classList.remove('active');
380
+ e.target.classList.add('active');
381
+ const filter = e.target.dataset.filter;
382
+ document.querySelectorAll('.equipment-card').forEach(card => {
383
+ card.style.display = (filter === 'all' || card.dataset.category === filter) ? 'block' : 'none';
384
+ });
 
385
  });
386
  }
387
  });
 
391
  '''
392
 
393
  ADMIN_TEMPLATE = '''
394
+ <!DOCTYPE html>
395
+ <html lang="ru">
396
+ <head>
397
+ <meta charset="UTF-8">
398
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
399
+ <title>Админ-панель - Раина</title>
400
+ <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
401
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
402
+ <style>
403
+ body { font-family: 'Poppins', sans-serif; background-color: #f4f7f9; color: #333; padding: 20px; line-height: 1.6; }
404
+ .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); }
405
+ .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;}
406
+ h1, h2, h3 { font-weight: 600; color: #6a0dad; margin-bottom: 15px; }
407
+ h1 { font-size: 1.8rem; }
408
+ h2 { font-size: 1.5rem; margin-top: 30px; display: flex; align-items: center; gap: 8px; }
409
+ .section { margin-bottom: 30px; padding: 20px; background-color: #fafafa; border: 1px solid #e9e9e9; border-radius: 8px; }
410
+ form { margin-bottom: 20px; }
411
+ label { font-weight: 500; margin-top: 10px; display: block; color: #555; font-size: 0.9rem;}
412
+ input[type="text"], input[type="number"], textarea, select { width: 100%; padding: 10px 12px; margin-top: 5px; border: 1px solid #ddd; border-radius: 6px; font-size: 0.95rem; box-sizing: border-box; }
413
+ input[type="file"] { padding: 8px; cursor: pointer; border: 1px solid #ddd;}
414
+ button, .button { padding: 10px 18px; border: none; border-radius: 6px; background-color: #9b59b6; color: white; font-weight: 500; cursor: pointer; transition: all 0.3s ease; margin-top: 15px; text-decoration: none; }
415
+ button:hover, .button:hover { background-color: #8e44ad; }
416
+ .delete-button { background-color: #e74c3c; }
417
+ .delete-button:hover { background-color: #c0392b; }
418
+ .item-list { display: grid; gap: 20px; }
419
+ .item { background: #fff; padding: 15px 20px; border-radius: 8px; border: 1px solid #eee; }
420
+ .item-actions { margin-top: 15px; display: flex; gap: 10px; flex-wrap: wrap; }
421
+ .edit-form-container { margin-top: 15px; padding: 20px; background: #fdf9ff; border: 1px dashed #ddd; border-radius: 6px; display: none; }
422
+ details { background-color: #fafafa; border: 1px solid #e9e9e9; border-radius: 8px; margin-bottom: 20px; }
423
+ details > summary { cursor: pointer; font-weight: 600; color: #8e44ad; display: block; padding: 15px; position: relative; list-style: none; }
424
+ details > summary::after { content: '\\f078'; font-family: 'Font Awesome 6 Free'; font-weight: 900; position: absolute; right: 20px; top: 50%; transform: translateY(-50%); }
425
+ details[open] > summary::after { transform: translateY(-50%) rotate(180deg); }
426
+ .photo-preview img { max-width: 70px; max-height: 70px; border-radius: 5px; margin: 5px 5px 0 0; object-fit: cover;}
427
+ .message { padding: 10px 15px; border-radius: 6px; margin-bottom: 15px; }
428
+ .message.success { background-color: #d4edda; color: #155724; }
429
+ .message.error { background-color: #f8d7da; color: #721c24; }
430
+ .message.warning { background-color: #fff3cd; color: #856404; }
431
+ </style>
432
+ </head>
433
+ <body>
434
+ <div class="container">
435
+ <div class="header"><h1><i class="fas fa-tools"></i> Админ-панель "Раина"</h1><a href="{{ url_for('landing') }}" class="button"><i class="fas fa-home"></i> Перейти на сайт</a></div>
436
+ {% with messages = get_flashed_messages(with_categories=true) %}{% if messages %}{% for category, message in messages %}<div class="message {{ category }}">{{ message }}</div>{% endfor %}{% endif %}{% endwith %}
437
+
438
+ <div class="section">
439
+ <h2><i class="fas fa-sync-alt"></i> Синхронизация</h2>
440
+ <form method="POST" action="{{ url_for('force_upload') }}" style="display: inline;"><button type="submit" class="button">Загрузить на сервер</button></form>
441
+ <form method="POST" action="{{ url_for('force_download') }}" style="display: inline;"><button type="submit" class="button">Скачать с сервера</button></form>
442
+ </div>
 
 
 
 
 
 
 
443
 
444
+ <div class="section">
445
+ <h2><i class="fas fa-star"></i> Реализованные проекты</h2>
446
+ <details><summary>Добавить проект</summary>
447
+ <form method="POST" enctype="multipart/form-data"><input type="hidden" name="action" value="add_project">
448
+ <label>Название*:</label><input type="text" name="title" required>
449
+ <label>Описание*:</label><textarea name="description" rows="3" required></textarea>
450
+ <label>Фото*:</label><input type="file" name="photo" accept="image/*" required>
451
+ <button type="submit">Добавить проект</button>
452
+ </form>
453
+ </details>
454
+ <div class="item-list">
455
+ {% for project in projects %}
456
+ <div class="item">
457
+ <p><strong>{{ project.title }}</strong>: {{ project.description }}</p>
458
+ <div class="item-actions">
459
+ <button onclick="toggleEditForm('edit-project-{{ loop.index0 }}')">Редактировать</button>
460
+ <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>
461
+ </div>
462
+ <div id="edit-project-{{ loop.index0 }}" class="edit-form-container">
463
+ <form method="POST" enctype="multipart/form-data"><input type="hidden" name="action" value="edit_project"><input type="hidden" name="index" value="{{ loop.index0 }}">
464
+ <label>Название*:</label><input type="text" name="title" value="{{ project.title }}" required>
465
+ <label>Описание*:</label><textarea name="description" rows="3" required>{{ project.description }}</textarea>
466
+ <label>Заменить фото:</label><input type="file" name="photo" accept="image/*">
467
+ <button type="submit">Сохранить</button>
468
+ </form>
469
+ </div>
470
  </div>
471
+ {% endfor %}
472
  </div>
473
+ </div>
474
 
475
+ <div class="section">
476
+ <h2><i class="fas fa-concierge-bell"></i> Услуги "под ключ"</h2>
477
+ <details><summary>Добавить услугу</summary>
478
+ <form method="POST" enctype="multipart/form-data"><input type="hidden" name="action" value="add_service">
479
+ <label>Заголовок*:</label><input type="text" name="title" required>
480
+ <label>Иконка (FontAwesome)*:</label><input type="text" name="icon" placeholder="fas fa-tools" required>
481
+ <label>Описание*:</label><textarea name="description" rows="3" required></textarea>
482
+ <label>Фото:</label><input type="file" name="photo" accept="image/*">
483
+ <button type="submit">Добавить услугу</button>
484
+ </form>
485
+ </details>
486
+ <div class="item-list">
487
+ {% for service in services %}
488
+ <div class="item">
489
+ <p><i class="{{ service.icon }} fa-fw"></i> <strong>{{ service.title }}</strong>: {{ service.description }}</p>
490
+ <div class="item-actions">
491
+ <button onclick="toggleEditForm('edit-service-{{ loop.index0 }}')">Редактировать</button>
492
+ <form method="POST" style="margin:0;"><input type="hidden" name="action" value="delete_service"><input type="hidden" name="index" value="{{ loop.index0 }}"><button type="submit" class="delete-button">Удалить</button></form>
493
+ </div>
494
+ <div id="edit-service-{{ loop.index0 }}" class="edit-form-container">
495
+ <form method="POST" enctype="multipart/form-data"><input type="hidden" name="action" value="edit_service"><input type="hidden" name="index" value="{{ loop.index0 }}">
496
+ <label>Заголовок*:</label><input type="text" name="title" value="{{ service.title }}" required>
497
+ <label>Иконка*:</label><input type="text" name="icon" value="{{ service.icon }}" required>
498
+ <label>Описание*:</label><textarea name="description" rows="3" required>{{ service.description }}</textarea>
499
+ <label>Заменить фото:</label><input type="file" name="photo" accept="image/*">
500
+ <button type="submit">Сохранить</button>
501
+ </form>
502
+ </div>
503
+ </div>
504
+ {% endfor %}
505
  </div>
506
+ </div>
507
 
508
+ <div class="section">
509
+ <h2><i class="fas fa-box-open"></i> Оборудование</h2>
510
+ <details><summary>Добавить категорию</summary>
511
+ <form method="POST"><input type="hidden" name="action" value="add_category"><label>Название:</label><input type="text" name="category_name" required><button type="submit">Добавить</button></form>
512
+ </details>
513
+ <div class="item-list">
514
+ {% for category in categories %}
515
+ <div class="item" style="display: flex; justify-content: space-between; align-items: center;">
516
+ <span>{{ category }}</span>
517
+ <form method="POST" style="margin: 0;"><input type="hidden" name="action" value="delete_category"><input type="hidden" name="category_name" value="{{ category }}"><button type="submit" class="delete-button" style="margin:0;">Удалить</button></form>
518
+ </div>
519
+ {% endfor %}
520
  </div>
521
 
522
+ <details style="margin-top:20px;"><summary>Добавить оборудование</summary>
523
+ <form method="POST" enctype="multipart/form-data"><input type="hidden" name="action" value="add_equipment">
524
+ <label>Название*:</label><input type="text" name="name" required>
525
+ <label>Цена (KGS)*:</label><input type="number" name="price" step="0.01" min="0" required>
526
+ <label>Категория:</label><select name="category"><option value="Без категории">Без категории</option>{% for cat in categories %}<option value="{{ cat }}">{{ cat }}</option>{% endfor %}</select>
527
+ <label>Фото:</label><input type="file" name="photo" accept="image/*">
528
+ <button type="submit">Добавить</button>
529
+ </form>
530
+ </details>
531
+ <div class="item-list">
532
+ {% for item in equipment %}
533
+ <div class="item">
534
+ <p><strong>{{ item.name }}</strong> ({{ item.category }}) - {{ "%.2f"|format(item.price) }} KGS</p>
535
+ <div class="item-actions">
536
+ <button onclick="toggleEditForm('edit-eq-{{ loop.index0 }}')">Редактировать</button>
537
+ <form method="POST" style="margin:0;"><input type="hidden" name="action" value="delete_equipment"><input type="hidden" name="index" value="{{ loop.index0 }}"><button type="submit" class="delete-button">Удалить</button></form>
538
+ </div>
539
+ <div id="edit-eq-{{ loop.index0 }}" class="edit-form-container">
540
+ <form method="POST" enctype="multipart/form-data"><input type="hidden" name="action" value="edit_equipment"><input type="hidden" name="index" value="{{ loop.index0 }}">
541
+ <label>Название*:</label><input type="text" name="name" value="{{ item.name }}" required>
542
+ <label>Цена (KGS)*:</label><input type="number" name="price" value="{{ item.price }}" step="0.01" min="0" required>
543
+ <label>Категория:</label><select name="category">{% for cat in categories %}<option value="{{ cat }}" {% if item.category == cat %}selected{% endif %}>{{ cat }}</option>{% endfor %}</select>
544
+ <label>Заменить фото:</label><input type="file" name="photo" accept="image/*">
545
+ <button type="submit">Сохранить</button>
546
+ </form>
547
+ </div>
548
+ </div>
549
+ {% endfor %}
550
  </div>
551
  </div>
552
+ <script>function toggleEditForm(id) { document.getElementById(id).style.display = document.getElementById(id).style.display === 'block' ? 'none' : 'block'; }</script>
 
 
 
 
 
553
  </body>
554
  </html>
555
  '''
 
560
  return render_template_string(
561
  LANDING_TEMPLATE,
562
  services=data.get('services', []),
563
+ equipment=data.get('equipment', []),
564
  categories=sorted(data.get('categories', [])),
565
  projects=data.get('projects', []),
566
  repo_id=REPO_ID,
 
572
  @app.route('/admin', methods=['GET', 'POST'])
573
  def admin():
574
  data = load_data()
575
+
576
  if request.method == 'POST':
577
  action = request.form.get('action')
578
  logging.info(f"Admin action: {action}")
579
  try:
580
  if action == 'add_category':
581
+ name = request.form.get('category_name', '').strip()
582
+ if name and name not in data['categories']:
583
+ data['categories'].append(name)
584
+ flash(f"Категория '{name}' добавлена.", 'success')
585
+ else: flash("Категория уже существует или пуста.", 'error')
586
+
 
 
587
  elif action == 'delete_category':
588
+ name = request.form.get('category_name')
589
+ if name in data['categories']:
590
+ data['categories'].remove(name)
591
+ flash(f"Категория '{name}' удалена.", 'success')
592
+
593
+ elif action in ['add_equipment', 'edit_equipment']:
594
+ name = request.form.get('name', '').strip()
595
+ price = round(float(request.form.get('price', 0)), 2)
596
+ category = request.form.get('category')
597
+ if not name or price <= 0:
598
+ flash("Название и цена обязательны.", 'error')
599
+ return redirect(url_for('admin'))
600
+
601
+ item_data = {'name': name, 'price': price, 'category': category}
602
+ photo = request.files.get('photo')
603
+
604
+ if action == 'add_equipment':
605
+ if photo and photo.filename:
606
+ item_data['photo'] = upload_photo_to_hf(photo, name, 'equipment')
607
+ data['equipment'].append(item_data)
608
+ flash(f"Оборудование '{name}' добавлено.", 'success')
609
+
610
+ else: # edit_equipment
611
+ index = int(request.form.get('index'))
612
+ original_item = data['equipment'][index]
613
+ if photo and photo.filename:
614
+ delete_photo_from_hf(original_item.get('photo'), 'equipment')
615
+ item_data['photo'] = upload_photo_to_hf(photo, name, 'equipment')
616
+ else:
617
+ item_data['photo'] = original_item.get('photo')
618
+ data['equipment'][index] = item_data
619
+ flash(f"Оборудование '{name}' обновлено.", 'success')
620
+
621
+ elif action == 'delete_equipment':
622
+ index = int(request.form.get('index'))
623
+ item = data['equipment'].pop(index)
624
+ delete_photo_from_hf(item.get('photo'), 'equipment')
625
+ flash(f"Оборудование '{item.get('name')}' удалено.", 'success')
626
 
627
+ elif action in ['add_service', 'edit_service']:
628
+ title = request.form.get('title', '').strip()
629
+ item_data = {'title': title, 'icon': request.form.get('icon'), 'description': request.form.get('description')}
630
+ photo = request.files.get('photo')
631
+ if action == 'add_service':
632
+ if photo and photo.filename:
633
+ item_data['photo'] = upload_photo_to_hf(photo, title, 'services')
634
+ data['services'].append(item_data)
635
  flash(f"Услуга '{title}' добавлена.", 'success')
636
+ else: # edit_service
637
+ index = int(request.form.get('index'))
638
+ original_item = data['services'][index]
639
+ if photo and photo.filename:
640
+ delete_photo_from_hf(original_item.get('photo'), 'services')
641
+ item_data['photo'] = upload_photo_to_hf(photo, title, 'services')
642
+ else:
643
+ item_data['photo'] = original_item.get('photo')
644
+ data['services'][index] = item_data
645
+ flash(f"Услуга '{title}' обновлена.", 'success')
646
+
 
 
 
 
647
  elif action == 'delete_service':
648
+ index = int(request.form.get('index'))
649
+ item = data['services'].pop(index)
650
+ delete_photo_from_hf(item.get('photo'), 'services')
651
+ flash(f"Услуга '{item.get('title')}' удалена.", 'success')
652
+
653
+ elif action in ['add_project', 'edit_project']:
654
+ title = request.form.get('title', '').strip()
655
+ item_data = {'title': title, 'description': request.form.get('description')}
656
+ photo = request.files.get('photo')
657
+ if action == 'add_project':
658
+ if photo and photo.filename:
659
+ item_data['photo'] = upload_photo_to_hf(photo, title, 'projects')
660
+ data['projects'].append(item_data)
661
+ flash(f"Проект '{title}' добавлен.", 'success')
662
+ else:
663
+ flash("Фото обязательно для нового проекта.", 'error')
664
+ else: # edit_project
665
+ index = int(request.form.get('index'))
666
+ original_item = data['projects'][index]
667
+ if photo and photo.filename:
668
+ delete_photo_from_hf(original_item.get('photo'), 'projects')
669
+ item_data['photo'] = upload_photo_to_hf(photo, title, 'projects')
670
+ else:
671
+ item_data['photo'] = original_item.get('photo')
672
+ data['projects'][index] = item_data
673
+ flash(f"Проект '{title}' обновлен.", 'success')
 
674
 
675
  elif action == 'delete_project':
676
+ index = int(request.form.get('index'))
677
+ item = data['projects'].pop(index)
678
+ delete_photo_from_hf(item.get('photo'), 'projects')
679
+ flash(f"Проект '{item.get('title')}' удален.", 'success')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
680
 
681
+ save_data(data)
682
  return redirect(url_for('admin'))
683
  except Exception as e:
684
  logging.error(f"Admin action '{action}' failed: {e}", exc_info=True)
685
+ flash(f"Произошла ошибка: {e}", 'error')
686
  return redirect(url_for('admin'))
687
 
688
  return render_template_string(
689
  ADMIN_TEMPLATE,
690
+ equipment=data.get('equipment', []),
691
  categories=sorted(data.get('categories', [])),
692
  services=data.get('services', []),
693
+ projects=data.get('projects', [])
 
694
  )
695
 
696
+ def upload_photo_to_hf(photo, item_name, folder):
697
+ if not photo or not photo.filename or not HF_TOKEN_WRITE:
698
+ return None
699
+ try:
700
+ api = HfApi()
701
+ safe_name = secure_filename(item_name.replace(' ', '_'))[:50]
702
+ ext = os.path.splitext(photo.filename)[1].lower()
703
+ photo_filename = f"{safe_name}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{ext}"
704
+
705
+ api.upload_file(
706
+ path_or_fileobj=photo, path_in_repo=f"{folder}/{photo_filename}",
707
+ repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE
708
+ )
709
+ logging.info(f"Uploaded photo {photo_filename} to {folder}")
710
+ return photo_filename
711
+ except Exception as e:
712
+ logging.error(f"Error uploading photo {photo.filename}: {e}")
713
+ flash(f"Ошибка загрузки фото {photo.filename}.", 'error')
714
+ return None
 
 
715
 
716
+ def delete_photo_from_hf(photo_filename, folder):
717
+ if not photo_filename or not HF_TOKEN_WRITE:
718
+ return
 
719
  try:
720
+ api = HfApi()
721
+ api.delete_files(
722
+ repo_id=REPO_ID, paths_in_repo=[f"{folder}/{photo_filename}"],
723
+ repo_type="dataset", token=HF_TOKEN_WRITE
724
+ )
725
+ logging.info(f"Deleted photo {photo_filename} from {folder}")
726
+ except HfHubHTTPError as e:
727
+ if e.response.status_code != 404:
728
+ logging.error(f"Error deleting photo {photo_filename}: {e}")
729
  except Exception as e:
730
+ logging.error(f"Error deleting photo {photo_filename}: {e}")
 
731
 
732
  @app.route('/force_upload', methods=['POST'])
733
  def force_upload():
 
737
 
738
  @app.route('/force_download', methods=['POST'])
739
  def force_download():
740
+ download_db_from_hf()
741
+ flash("Данные скачаны с сервера.", 'success')
 
 
742
  return redirect(url_for('admin'))
743
 
744
  if __name__ == '__main__':
745
+ logging.info("Application starting up...")
746
  download_db_from_hf()
747
  if HF_TOKEN_WRITE:
748
  threading.Thread(target=periodic_backup, daemon=True).start()