Shveiauto commited on
Commit
a27dbf6
·
verified ·
1 Parent(s): 20eb6f9

Upload Optomshoptxt.txt

Browse files
Files changed (1) hide show
  1. Optomshoptxt.txt +2101 -0
Optomshoptxt.txt ADDED
@@ -0,0 +1,2101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ from flask import Flask, render_template_string, request, redirect, url_for, send_file
3
+ import json
4
+ import os
5
+ import logging
6
+ import threading
7
+ import time
8
+ from datetime import datetime
9
+ from huggingface_hub import HfApi, hf_hub_download
10
+ from huggingface_hub.utils import RepositoryNotFoundError
11
+ from werkzeug.utils import secure_filename
12
+ from urllib.parse import quote
13
+
14
+ app = Flask(__name__)
15
+ DATA_FILE = 'data.json'
16
+
17
+ REPO_ID = "Kgshop/Mebelhause"
18
+ HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
19
+ HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
20
+
21
+ LOGO_URL = "https://huggingface.co/spaces/Mebelhause/Kg/resolve/main/Screenshot_20250411-112027.png"
22
+ WHATSAPP_NUMBER = "+996700253966"
23
+
24
+ logging.basicConfig(level=logging.INFO)
25
+
26
+ def load_data():
27
+ try:
28
+ download_db_from_hf()
29
+ with open(DATA_FILE, 'r', encoding='utf-8') as file:
30
+ data = json.load(file)
31
+ logging.info("Данные успешно загружены из JSON")
32
+ if not isinstance(data, dict) or 'products' not in data or 'categories' not in data:
33
+ logging.warning("Структура данных некорректна, используется структура по умолчанию.")
34
+ return {'products': [], 'categories': []}
35
+ # Ensure products and categories are lists
36
+ if not isinstance(data.get('products'), list):
37
+ data['products'] = []
38
+ if not isinstance(data.get('categories'), list):
39
+ data['categories'] = []
40
+ return data
41
+ except FileNotFoundError:
42
+ logging.warning("Локальный файл базы данных не найден. Создание пустой базы.")
43
+ return {'products': [], 'categories': []}
44
+ except json.JSONDecodeError:
45
+ logging.error("Ошибка: Невозможно декодировать JSON файл. Возврат пустой базы.")
46
+ return {'products': [], 'categories': []}
47
+ except RepositoryNotFoundError:
48
+ logging.error("Репозиторий Hugging Face не найден. Создание локальной базы данных.")
49
+ # Create an empty file if it doesn't exist locally after repo not found
50
+ if not os.path.exists(DATA_FILE):
51
+ with open(DATA_FILE, 'w', encoding='utf-8') as f:
52
+ json.dump({'products': [], 'categories': []}, f)
53
+ return {'products': [], 'categories': []}
54
+ except Exception as e:
55
+ logging.error(f"Произошла ошибка при загрузке данных: {e}. Возврат пустой базы.")
56
+ return {'products': [], 'categories': []}
57
+
58
+ def save_data(data):
59
+ try:
60
+ # Ensure structure is correct before saving
61
+ if 'products' not in data or not isinstance(data['products'], list):
62
+ data['products'] = []
63
+ if 'categories' not in data or not isinstance(data['categories'], list):
64
+ data['categories'] = []
65
+
66
+ with open(DATA_FILE, 'w', encoding='utf-8') as file:
67
+ json.dump(data, file, ensure_ascii=False, indent=4)
68
+ logging.info("Данные успешно сохранены в JSON")
69
+ upload_db_to_hf()
70
+ except Exception as e:
71
+ logging.error(f"Ошибка при сохранении данных: {e}")
72
+ # Optionally re-raise or handle differently
73
+ # raise
74
+
75
+ def upload_db_to_hf():
76
+ if not HF_TOKEN_WRITE:
77
+ logging.warning("HF_TOKEN (write) не установлен. Загрузка на Hugging Face пропущена.")
78
+ return
79
+ if not os.path.exists(DATA_FILE):
80
+ logging.warning(f"Файл {DATA_FILE} не найден для загрузки.")
81
+ return
82
+ try:
83
+ api = HfApi()
84
+ api.upload_file(
85
+ path_or_fileobj=DATA_FILE,
86
+ path_in_repo=DATA_FILE,
87
+ repo_id=REPO_ID,
88
+ repo_type="dataset",
89
+ token=HF_TOKEN_WRITE,
90
+ commit_message=f"Автоматическое резервное копирование базы данных {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
91
+ )
92
+ logging.info("Резервная копия JSON базы успешно загружена на Hugging Face.")
93
+ except RepositoryNotFoundError:
94
+ logging.error(f"Репозиторий {REPO_ID} не найден на Hugging Face. Не удалось загрузить.")
95
+ except Exception as e:
96
+ logging.error(f"Ошибка при загрузке резервной копии на Hugging Face: {e}")
97
+
98
+
99
+ def download_db_from_hf():
100
+ if not HF_TOKEN_READ:
101
+ logging.warning("HF_TOKEN_READ не установлен. Скачивание с Hugging Face пропущено.")
102
+ # Attempt to create an empty file if none exists
103
+ if not os.path.exists(DATA_FILE):
104
+ save_data({'products': [], 'categories': []}) # Save an empty structure
105
+ return
106
+ try:
107
+ hf_hub_download(
108
+ repo_id=REPO_ID,
109
+ filename=DATA_FILE,
110
+ repo_type="dataset",
111
+ token=HF_TOKEN_READ,
112
+ local_dir=".",
113
+ local_dir_use_symlinks=False,
114
+ force_download=True, # Ensure we get the latest version
115
+ resume_download=False # Avoid issues with partial downloads
116
+ )
117
+ logging.info("JSON база успешно скачана из Hugging Face.")
118
+ # Verify file integrity after download
119
+ try:
120
+ with open(DATA_FILE, 'r', encoding='utf-8') as f:
121
+ json.load(f)
122
+ logging.info("Проверка целостности JSON после скачивания: OK")
123
+ except (json.JSONDecodeError, FileNotFoundError) as e:
124
+ logging.error(f"Ошибка проверки JSON после скачивания: {e}. Файл может быть поврежден или пуст.")
125
+ # Attempt to create a default empty file if it's broken
126
+ save_data({'products': [], 'categories': []})
127
+
128
+ except RepositoryNotFoundError as e:
129
+ logging.error(f"Репозиторий {REPO_ID} не найден на Hugging Face: {e}")
130
+ if not os.path.exists(DATA_FILE):
131
+ save_data({'products': [], 'categories': []}) # Save an empty structure
132
+ # Do not raise here, allow fallback to local/empty data
133
+ except Exception as e:
134
+ # Catch specific hf_hub exceptions if needed, e.g., HTTPError
135
+ logging.error(f"Ошибка при скачивании JSON базы из Hugging Face: {e}")
136
+ if not os.path.exists(DATA_FILE):
137
+ save_data({'products': [], 'categories': []}) # Save an empty structure
138
+ # Do not raise here, allow fallback to local/empty data
139
+
140
+ def periodic_backup():
141
+ while True:
142
+ time.sleep(800) # Sleep first to avoid immediate backup on start
143
+ logging.info("Запуск периодического резервного копирования...")
144
+ # Ensure data is loaded before backup attempt
145
+ try:
146
+ load_data() # Reload potentially updated data
147
+ except Exception as e:
148
+ logging.error(f"Ошибка загрузки данных перед бэкапом: {e}")
149
+ # Now attempt upload
150
+ upload_db_to_hf()
151
+
152
+ # --- Global Styles ---
153
+ GLOBAL_STYLES = """
154
+ <style>
155
+ :root {
156
+ --bg-beige: #f5f5dc;
157
+ --text-dark-brown: #4d2c1a; /* Darker brown for text */
158
+ --accent-wood: #8b4513; /* SaddleBrown */
159
+ --accent-light-wood: #deb887; /* BurlyWood */
160
+ --button-bg: var(--accent-wood);
161
+ --button-hover-bg: #a0522d; /* Sienna - slightly lighter */
162
+ --card-bg: #ffffff;
163
+ --border-color: #e0d8c7; /* Lighter border */
164
+ --header-bg: #ffffff; /* White header */
165
+ --footer-bg: var(--text-dark-brown);
166
+ --footer-text: var(--bg-beige);
167
+ --link-color: var(--accent-wood);
168
+ --link-hover-color: var(--button-hover-bg);
169
+
170
+ --font-primary: 'Poppins', sans-serif;
171
+ }
172
+ * {
173
+ margin: 0;
174
+ padding: 0;
175
+ box-sizing: border-box;
176
+ }
177
+ body {
178
+ font-family: var(--font-primary);
179
+ background-color: var(--bg-beige);
180
+ color: var(--text-dark-brown);
181
+ line-height: 1.6;
182
+ font-size: 16px;
183
+ }
184
+ .container {
185
+ max-width: 1300px;
186
+ margin: 0 auto;
187
+ padding: 20px;
188
+ }
189
+ .header {
190
+ display: flex;
191
+ justify-content: space-between;
192
+ align-items: center;
193
+ padding: 15px 20px;
194
+ background-color: var(--header-bg);
195
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
196
+ position: sticky;
197
+ top: 0;
198
+ z-index: 1000;
199
+ }
200
+ .header-logo {
201
+ height: 60px; /* Adjust height as needed */
202
+ width: auto; /* Maintain aspect ratio */
203
+ object-fit: contain; /* Ensure logo fits well */
204
+ }
205
+ .header nav a {
206
+ color: var(--text-dark-brown);
207
+ text-decoration: none;
208
+ margin-left: 25px;
209
+ font-weight: 500;
210
+ transition: color 0.3s ease;
211
+ font-size: 1.1rem;
212
+ }
213
+ .header nav a:hover, .header nav a.active {
214
+ color: var(--accent-wood);
215
+ font-weight: 600;
216
+ }
217
+ .main-content {
218
+ padding: 40px 0;
219
+ }
220
+ h1, h2, h3 {
221
+ color: var(--text-dark-brown);
222
+ margin-bottom: 1rem;
223
+ font-weight: 600;
224
+ }
225
+ h1 { font-size: 2.5rem; text-align: center; margin-bottom: 2rem; }
226
+ h2 { font-size: 2rem; margin-bottom: 1.5rem; border-bottom: 2px solid var(--accent-light-wood); padding-bottom: 0.5rem;}
227
+ h3 { font-size: 1.5rem; }
228
+
229
+ button, .button-link {
230
+ padding: 12px 25px;
231
+ border: none;
232
+ border-radius: 8px;
233
+ background-color: var(--button-bg);
234
+ color: white;
235
+ font-weight: 500;
236
+ cursor: pointer;
237
+ transition: background-color 0.3s ease, transform 0.2s ease;
238
+ text-decoration: none; /* For button-link */
239
+ display: inline-block; /* For button-link */
240
+ font-size: 1rem;
241
+ text-align: center;
242
+ }
243
+ button:hover, .button-link:hover {
244
+ background-color: var(--button-hover-bg);
245
+ transform: translateY(-2px);
246
+ }
247
+ .whatsapp-float {
248
+ position: fixed;
249
+ width: 60px;
250
+ height: 60px;
251
+ bottom: 30px;
252
+ right: 30px;
253
+ background-color: #25D366;
254
+ color: #FFF;
255
+ border-radius: 50px;
256
+ text-align: center;
257
+ font-size: 30px;
258
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
259
+ z-index: 100;
260
+ display: flex;
261
+ justify-content: center;
262
+ align-items: center;
263
+ text-decoration: none;
264
+ transition: transform 0.3s ease;
265
+ }
266
+ .whatsapp-float:hover {
267
+ transform: scale(1.1);
268
+ }
269
+ .whatsapp-float i {
270
+ margin-top: 0; /* Adjust if needed */
271
+ }
272
+ .footer {
273
+ background-color: var(--footer-bg);
274
+ color: var(--footer-text);
275
+ text-align: center;
276
+ padding: 20px;
277
+ margin-top: 40px;
278
+ }
279
+ .footer p {
280
+ margin: 0;
281
+ font-size: 0.9rem;
282
+ }
283
+
284
+ /* Form Styles */
285
+ .form-section {
286
+ background: var(--card-bg);
287
+ padding: 30px;
288
+ border-radius: 15px;
289
+ box-shadow: 0 4px 15px rgba(0,0,0,0.1);
290
+ margin-bottom: 30px;
291
+ }
292
+ label {
293
+ font-weight: 500;
294
+ margin-top: 15px;
295
+ display: block;
296
+ color: var(--text-dark-brown);
297
+ }
298
+ input[type="text"],
299
+ input[type="number"],
300
+ input[type="file"],
301
+ textarea,
302
+ select {
303
+ width: 100%;
304
+ padding: 12px;
305
+ margin-top: 8px;
306
+ border: 1px solid var(--border-color);
307
+ border-radius: 8px;
308
+ font-size: 1rem;
309
+ background-color: #fff;
310
+ color: var(--text-dark-brown);
311
+ transition: border-color 0.3s ease, box-shadow 0.3s ease;
312
+ }
313
+ input:focus, textarea:focus, select:focus {
314
+ border-color: var(--accent-wood);
315
+ box-shadow: 0 0 0 3px rgba(139, 69, 19, 0.2); /* SaddleBrown with opacity */
316
+ outline: none;
317
+ }
318
+ textarea {
319
+ min-height: 100px;
320
+ resize: vertical;
321
+ }
322
+ .form-button {
323
+ margin-top: 20px;
324
+ }
325
+ .delete-button {
326
+ background-color: #ef4444;
327
+ }
328
+ .delete-button:hover {
329
+ background-color: #dc2626;
330
+ }
331
+ .add-color-btn {
332
+ background-color: #10b981;
333
+ margin-left: 10px;
334
+ padding: 8px 15px;
335
+ }
336
+ .add-color-btn:hover {
337
+ background-color: #059669;
338
+ }
339
+ .color-input-group {
340
+ display: flex;
341
+ align-items: center;
342
+ gap: 10px;
343
+ margin-top: 5px;
344
+ }
345
+ .color-input-group input {
346
+ flex-grow: 1;
347
+ }
348
+ .color-input-group button {
349
+ flex-shrink: 0;
350
+ margin-top: 8px; /* Align with input's margin */
351
+ padding: 10px 10px; /* Smaller padding */
352
+ font-size: 0.9rem;
353
+ }
354
+ .remove-color-btn {
355
+ background-color: #ef4444;
356
+ color: white;
357
+ border: none;
358
+ border-radius: 50%;
359
+ width: 25px;
360
+ height: 25px;
361
+ font-size: 14px;
362
+ line-height: 25px; /* Center the 'X' */
363
+ text-align: center;
364
+ cursor: pointer;
365
+ padding: 0;
366
+ margin-left: 5px;
367
+ }
368
+
369
+
370
+ /* Modal Styles */
371
+ .modal {
372
+ display: none;
373
+ position: fixed;
374
+ z-index: 1001;
375
+ left: 0;
376
+ top: 0;
377
+ width: 100%;
378
+ height: 100%;
379
+ overflow: auto;
380
+ background-color: rgba(0,0,0,0.6);
381
+ backdrop-filter: blur(5px);
382
+ }
383
+ .modal-content {
384
+ background-color: var(--card-bg);
385
+ margin: 5% auto;
386
+ padding: 30px;
387
+ border-radius: 15px;
388
+ width: 90%;
389
+ max-width: 700px;
390
+ box-shadow: 0 10px 30px rgba(0,0,0,0.2);
391
+ animation: slideIn 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
392
+ position: relative;
393
+ }
394
+ @keyframes slideIn {
395
+ from { transform: translateY(-50px); opacity: 0; }
396
+ to { transform: translateY(0); opacity: 1; }
397
+ }
398
+ .close {
399
+ position: absolute;
400
+ top: 15px;
401
+ right: 20px;
402
+ color: var(--text-dark-brown);
403
+ font-size: 2rem;
404
+ font-weight: bold;
405
+ cursor: pointer;
406
+ transition: color 0.3s;
407
+ }
408
+ .close:hover, .close:focus {
409
+ color: var(--accent-wood);
410
+ text-decoration: none;
411
+ }
412
+ .modal-content h2 {
413
+ margin-top: 0;
414
+ margin-bottom: 1.5rem;
415
+ border-bottom: 1px solid var(--border-color);
416
+ padding-bottom: 0.8rem;
417
+ }
418
+ .modal-content p {
419
+ margin-bottom: 1rem;
420
+ }
421
+ .modal-content strong {
422
+ color: var(--accent-wood);
423
+ }
424
+
425
+ /* Swiper Styles for Modal */
426
+ .swiper-container {
427
+ width: 100%;
428
+ max-width: 450px; /* Adjust as needed */
429
+ margin: 0 auto 25px;
430
+ border-radius: 10px;
431
+ overflow: hidden;
432
+ background-color: #eee; /* Placeholder background */
433
+ }
434
+ .swiper-slide {
435
+ text-align: center;
436
+ background: #fff;
437
+ display: flex;
438
+ justify-content: center;
439
+ align-items: center;
440
+ height: 350px; /* Fixed height for slides */
441
+ }
442
+ .swiper-slide img {
443
+ display: block;
444
+ max-width: 100%;
445
+ max-height: 100%;
446
+ object-fit: contain;
447
+ border-radius: 10px;
448
+ }
449
+ .swiper-pagination-bullet-active {
450
+ background: var(--accent-wood) !important;
451
+ }
452
+ .swiper-button-next, .swiper-button-prev {
453
+ color: var(--accent-wood) !important;
454
+ background-color: rgba(255, 255, 255, 0.7);
455
+ border-radius: 50%;
456
+ width: 40px !important;
457
+ height: 40px !important;
458
+ transition: background-color 0.3s ease;
459
+ }
460
+ .swiper-button-next:hover, .swiper-button-prev:hover {
461
+ background-color: rgba(255, 255, 255, 0.9);
462
+ }
463
+ .swiper-button-next::after, .swiper-button-prev::after {
464
+ font-size: 20px !important;
465
+ font-weight: bold;
466
+ }
467
+
468
+ </style>
469
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
470
+ <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap" rel="stylesheet">
471
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.css">
472
+ """
473
+
474
+ # --- Landing Page ---
475
+ @app.route('/')
476
+ def landing():
477
+ landing_html = '''
478
+ <!DOCTYPE html>
479
+ <html lang="ru">
480
+ <head>
481
+ <meta charset="UTF-8">
482
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
483
+ <title>Mebel Hause KG - Изготовление мебели на заказ</title>
484
+ ''' + GLOBAL_STYLES + '''
485
+ <style>
486
+ .hero {
487
+ background: linear-gradient(rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.3)), url('https://images.unsplash.com/photo-1555041469-a586c61ea9bc?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80') no-repeat center center/cover; /* Example background */
488
+ color: white;
489
+ padding: 100px 20px;
490
+ text-align: center;
491
+ border-radius: 0 0 20px 20px; /* Rounded bottom corners */
492
+ }
493
+ .hero h1 {
494
+ font-size: 3rem;
495
+ margin-bottom: 1rem;
496
+ color: white;
497
+ text-shadow: 2px 2px 8px rgba(0,0,0,0.6);
498
+ border-bottom: none; /* Override global h1 style */
499
+ }
500
+ .hero p {
501
+ font-size: 1.3rem;
502
+ margin-bottom: 2rem;
503
+ text-shadow: 1px 1px 6px rgba(0,0,0,0.7);
504
+ }
505
+ .features {
506
+ display: grid;
507
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
508
+ gap: 30px;
509
+ margin-top: 40px;
510
+ }
511
+ .feature-item {
512
+ background: var(--card-bg);
513
+ padding: 25px;
514
+ border-radius: 15px;
515
+ text-align: center;
516
+ box-shadow: 0 4px 15px rgba(0,0,0,0.08);
517
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
518
+ }
519
+ .feature-item:hover {
520
+ transform: translateY(-5px);
521
+ box-shadow: 0 6px 20px rgba(0,0,0,0.12);
522
+ }
523
+ .feature-item i {
524
+ font-size: 3rem;
525
+ color: var(--accent-wood);
526
+ margin-bottom: 1rem;
527
+ }
528
+ .feature-item h3 {
529
+ margin-bottom: 0.5rem;
530
+ }
531
+ </style>
532
+ </head>
533
+ <body>
534
+ <header class="header">
535
+ <img src="{{ logo_url }}" alt="Mebel Hause KG Logo" class="header-logo">
536
+ <nav>
537
+ <a href="#calculator">Калькулятор</a>
538
+ <a href="{{ url_for('catalog') }}">Каталог</a>
539
+ <a href="#about">О нас</a>
540
+ <a href="#contact">Контакты</a>
541
+ </nav>
542
+ </header>
543
+
544
+ <section class="hero">
545
+ <h1>Mebel Hause KG</h1>
546
+ <p>Создаем мебель вашей мечты с любовью и мастерством</p>
547
+ <a href="{{ url_for('catalog') }}" class="button-link">Смотреть готовые изделия</a>
548
+ </section>
549
+
550
+ <main class="main-content container">
551
+ <section id="about">
552
+ <h2>Почему выбирают нас?</h2>
553
+ <div class="features">
554
+ <div class="feature-item">
555
+ <i class="fas fa-couch"></i>
556
+ <h3>Индивидуальный дизайн</h3>
557
+ <p>Разработаем уникальный проект под ваш интерьер и пожелания.</p>
558
+ </div>
559
+ <div class="feature-item">
560
+ <i class="fas fa-ruler-combined"></i>
561
+ <h3>Точные замеры</h3>
562
+ <p>Гарантируем идеальное соответствие мебели вашему пространству.</p>
563
+ </div>
564
+ <div class="feature-item">
565
+ <i class="fas fa-check-circle"></i>
566
+ <h3>Качественные материалы</h3>
567
+ <p>Используем только проверенные и долговечные материалы.</p>
568
+ </div>
569
+ <div class="feature-item">
570
+ <i class="fas fa-shipping-fast"></i>
571
+ <h3>Своевременная доставка</h3>
572
+ <p>Доставим и соберем вашу новую мебель точно в срок.</p>
573
+ </div>
574
+ </div>
575
+ </section>
576
+
577
+ <section id="calculator" class="form-section">
578
+ <h2>Рассчитать стоимость мебели</h2>
579
+ <p>Заполните форму ниже, и мы свяжемся с вами для уточнения деталей и расчета.</p>
580
+ <form id="calculator-form">
581
+ <label for="furniture-type">Вид мебели:</label>
582
+ <select id="furniture-type" name="furniture-type">
583
+ <option value="Кухня">Кухня</option>
584
+ <option value="Шкаф">Шкаф (купе, распашной)</option>
585
+ <option value="ТВ Зона">ТВ Зона</option>
586
+ <option value="Кровать">Кровать</option>
587
+ <option value="Стол">Стол (обеденный, письменный, журнальный)</option>
588
+ <option value="Диван">Диван</option>
589
+ <option value="Прихожая">Прихожая</option>
590
+ <option value="Детская">Детская мебель</option>
591
+ <option value="Другое">Другое (укажите в пожеланиях)</option>
592
+ </select>
593
+
594
+ <label for="material">Предпочитаемый материал:</label>
595
+ <select id="material" name="material">
596
+ <option value="ЛДСП">ЛДСП (Ламинированная ДСП)</option>
597
+ <option value="МДФ">МДФ (крашеный, пленочный, шпонированный)</option>
598
+ <option value="Массив дерева">Массив дерева</option>
599
+ <option value="Шпон">Шпон</option>
600
+ <option value="Пластик/Акрил">Пластик / Акриловые панели</option>
601
+ <option value="Не уверен">Не уверен / Нужна консультация</option>
602
+ </select>
603
+
604
+ <label for="dimensions">Примерные размеры (ШхВхГ в см) или площадь:</label>
605
+ <input type="text" id="dimensions" name="dimensions" placeholder="Например: 200x240x60 см или 'стена 3 метра'">
606
+
607
+ <label for="wishes">Дополнительные пожелания:</label>
608
+ <textarea id="wishes" name="wishes" placeholder="Например: цвет, фурнитура, особенности конструкции, стиль..."></textarea>
609
+
610
+ <button type="button" class="form-button" onclick="generateWhatsAppMessage()">Отправить запрос на расчет</button>
611
+ </form>
612
+ </section>
613
+
614
+ <section id="contact">
615
+ <h2>Свяжитесь с нами</h2>
616
+ <p>Готовы обсудить ваш проект? Напишите нам в WhatsApp или позвоните!</p>
617
+ <p style="font-size: 1.2rem; font-weight: 600;">Телефон / WhatsApp: <a href="https://wa.me/{{ whatsapp_number }}" target="_blank" style="color: var(--link-color);">{{ whatsapp_number_display }}</a></p>
618
+ <p>Мы находимся в Бишкеке.</p>
619
+ </section>
620
+
621
+ </main>
622
+
623
+ <footer class="footer">
624
+ <p>© {{ current_year }} Mebel Hause KG. Все права защищены.</p>
625
+ </footer>
626
+
627
+ <a href="https://wa.me/{{ whatsapp_number }}" class="whatsapp-float" target="_blank">
628
+ <i class="fab fa-whatsapp"></i>
629
+ </a>
630
+
631
+ <script>
632
+ function generateWhatsAppMessage() {
633
+ const furnitureType = document.getElementById('furniture-type').value;
634
+ const material = document.getElementById('material').value;
635
+ const dimensions = document.getElementById('dimensions').value;
636
+ const wishes = document.getElementById('wishes').value;
637
+ const whatsappNumber = "{{ whatsapp_number }}";
638
+
639
+ let message = `Здравствуйте, Mebel Hause KG!\\n\\nХочу рассчитать стоимость мебели:\\n`;
640
+ message += `*Тип:* ${furnitureType}\\n`;
641
+ message += `*Материал:* ${material}\\n`;
642
+ if (dimensions) {
643
+ message += `*Размеры/Площадь:* ${dimensions}\\n`;
644
+ }
645
+ if (wishes) {
646
+ message += `*Пожелания:* ${wishes}\\n`;
647
+ }
648
+ message += `\\nЖду вашего ответа.`;
649
+
650
+ const encodedMessage = encodeURIComponent(message);
651
+ const whatsappUrl = `https://api.whatsapp.com/send?phone=${whatsappNumber}&text=${encodedMessage}`;
652
+ // const whatsappUrl = `https://wa.me/${whatsappNumber}?text=${encodedMessage}`; // Alternative format
653
+
654
+ window.open(whatsappUrl, '_blank');
655
+ }
656
+
657
+ // Active link highlighting
658
+ document.addEventListener("DOMContentLoaded", function() {
659
+ const navLinks = document.querySelectorAll('.header nav a');
660
+ const currentPath = window.location.pathname;
661
+
662
+ navLinks.forEach(link => {
663
+ // Simple check for landing page links (using hash)
664
+ if (link.getAttribute('href').startsWith('#') && currentPath === '/') {
665
+ // Could add logic for scrollspy here if needed
666
+ } else if (link.getAttribute('href') === currentPath) {
667
+ link.classList.add('active');
668
+ }
669
+ });
670
+ });
671
+ </script>
672
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/js/all.min.js"></script>
673
+ </body>
674
+ </html>
675
+ '''
676
+ # Format number for display
677
+ whatsapp_display = f"+{WHATSAPP_NUMBER[0]} ({WHATSAPP_NUMBER[1:4]}) {WHATSAPP_NUMBER[4:7]}-{WHATSAPP_NUMBER[7:10]}-{WHATSAPP_NUMBER[10:]}"
678
+ return render_template_string(
679
+ landing_html,
680
+ logo_url=LOGO_URL,
681
+ whatsapp_number=WHATSAPP_NUMBER,
682
+ whatsapp_number_display=whatsapp_display,
683
+ current_year=datetime.now().year
684
+ )
685
+
686
+ # --- Catalog Page ---
687
+ @app.route('/catalog')
688
+ def catalog():
689
+ data = load_data()
690
+ products = data.get('products', [])
691
+ categories = data.get('categories', [])
692
+
693
+ catalog_html = '''
694
+ <!DOCTYPE html>
695
+ <html lang="ru">
696
+ <head>
697
+ <meta charset="UTF-8">
698
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
699
+ <title>Каталог готовой мебели - Mebel Hause KG</title>
700
+ ''' + GLOBAL_STYLES + '''
701
+ <style>
702
+ .filters-container {
703
+ margin: 30px 0;
704
+ display: flex;
705
+ flex-wrap: wrap;
706
+ gap: 10px;
707
+ justify-content: center;
708
+ padding-bottom: 20px;
709
+ border-bottom: 1px solid var(--border-color);
710
+ }
711
+ .search-container {
712
+ margin: 20px 0;
713
+ text-align: center;
714
+ }
715
+ #search-input {
716
+ width: 90%;
717
+ max-width: 600px;
718
+ padding: 12px 18px;
719
+ font-size: 1rem;
720
+ border: 1px solid var(--border-color);
721
+ border-radius: 8px;
722
+ outline: none;
723
+ box-shadow: 0 2px 5px rgba(0,0,0,0.05);
724
+ transition: all 0.3s ease;
725
+ }
726
+ #search-input:focus {
727
+ border-color: var(--accent-wood);
728
+ box-shadow: 0 0 0 3px rgba(139, 69, 19, 0.2);
729
+ }
730
+ .category-filter {
731
+ padding: 8px 16px;
732
+ border: 1px solid var(--border-color);
733
+ border-radius: 20px; /* Pill shape */
734
+ background-color: var(--card-bg);
735
+ color: var(--text-dark-brown);
736
+ cursor: pointer;
737
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
738
+ font-size: 0.9rem;
739
+ font-weight: 500;
740
+ }
741
+ .category-filter.active, .category-filter:hover {
742
+ background-color: var(--accent-wood);
743
+ color: white;
744
+ border-color: var(--accent-wood);
745
+ box-shadow: 0 2px 10px rgba(139, 69, 19, 0.3);
746
+ }
747
+ .products-grid {
748
+ display: grid;
749
+ /* Responsive columns: 1 on small, 2 on medium, 3-4 on large */
750
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
751
+ gap: 25px;
752
+ padding: 10px 0; /* Remove side padding */
753
+ }
754
+ .product {
755
+ background: var(--card-bg);
756
+ border-radius: 15px;
757
+ padding: 0; /* Remove padding, handle inside */
758
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
759
+ transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.3s ease;
760
+ overflow: hidden; /* Ensure content respects border radius */
761
+ display: flex;
762
+ flex-direction: column;
763
+ }
764
+ .product:hover {
765
+ transform: translateY(-5px) scale(1.02);
766
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12);
767
+ }
768
+ .product-image {
769
+ width: 100%;
770
+ aspect-ratio: 4 / 3; /* Adjust aspect ratio */
771
+ background-color: #f0f0f0; /* Light placeholder bg */
772
+ overflow: hidden;
773
+ display: flex;
774
+ justify-content: center;
775
+ align-items: center;
776
+ }
777
+ .product-image img {
778
+ width: 100%;
779
+ height: 100%;
780
+ object-fit: cover; /* Cover the area */
781
+ transition: transform 0.4s ease;
782
+ }
783
+ .product:hover .product-image img {
784
+ transform: scale(1.05); /* Slight zoom on hover */
785
+ }
786
+ .product-info {
787
+ padding: 15px;
788
+ text-align: center;
789
+ flex-grow: 1; /* Allow info to take remaining space */
790
+ display: flex;
791
+ flex-direction: column;
792
+ justify-content: space-between; /* Push buttons down */
793
+ }
794
+ .product-info-top {
795
+ margin-bottom: 15px; /* Space before buttons */
796
+ }
797
+ .product h2 {
798
+ font-size: 1.1rem; /* Slightly smaller */
799
+ font-weight: 600;
800
+ margin: 0 0 8px 0; /* Adjust margin */
801
+ white-space: nowrap;
802
+ overflow: hidden;
803
+ text-overflow: ellipsis;
804
+ border-bottom: none; /* Override global */
805
+ padding-bottom: 0;
806
+ }
807
+ .product-price {
808
+ font-size: 1.2rem;
809
+ color: var(--accent-wood); /* Accent color for price */
810
+ font-weight: 700;
811
+ margin: 5px 0;
812
+ }
813
+ .product-description {
814
+ font-size: 0.9rem;
815
+ color: #555; /* Slightly lighter text */
816
+ margin-bottom: 15px;
817
+ overflow: hidden;
818
+ text-overflow: ellipsis;
819
+ /* Limit description lines */
820
+ display: -webkit-box;
821
+ -webkit-line-clamp: 2; /* Show max 2 lines */
822
+ -webkit-box-orient: vertical;
823
+ }
824
+ .product-buttons {
825
+ display: flex;
826
+ gap: 10px; /* Space between buttons */
827
+ margin-top: auto; /* Push to bottom */
828
+ }
829
+ .product-button {
830
+ flex: 1; /* Make buttons equal width */
831
+ padding: 10px;
832
+ border: none;
833
+ border-radius: 8px;
834
+ background-color: var(--button-bg);
835
+ color: white;
836
+ font-size: 0.9rem;
837
+ font-weight: 500;
838
+ cursor: pointer;
839
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
840
+ text-align: center;
841
+ text-decoration: none;
842
+ }
843
+ .product-button:hover {
844
+ background-color: var(--button-hover-bg);
845
+ transform: translateY(-2px);
846
+ }
847
+ .add-to-cart {
848
+ background-color: #10b981; /* Green for cart */
849
+ }
850
+ .add-to-cart:hover {
851
+ background-color: #059669;
852
+ box-shadow: 0 4px 15px rgba(5, 150, 105, 0.4);
853
+ }
854
+ #cart-button {
855
+ position: fixed;
856
+ bottom: 30px; /* Match whatsapp button height */
857
+ right: 100px; /* Position next to whatsapp */
858
+ background-color: var(--accent-light-wood); /* Light wood color */
859
+ color: var(--text-dark-brown);
860
+ border: none;
861
+ border-radius: 50%;
862
+ width: 60px;
863
+ height: 60px;
864
+ font-size: 1.5rem; /* Larger icon */
865
+ cursor: pointer;
866
+ display: none; /* Hidden by default */
867
+ justify-content: center;
868
+ align-items: center;
869
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
870
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
871
+ z-index: 1000;
872
+ }
873
+ #cart-button:hover {
874
+ transform: scale(1.1);
875
+ background-color: var(--accent-wood);
876
+ color: white;
877
+ }
878
+ #cart-count {
879
+ position: absolute;
880
+ top: -5px;
881
+ right: -5px;
882
+ background-color: #ef4444; /* Red badge */
883
+ color: white;
884
+ border-radius: 50%;
885
+ padding: 2px 6px;
886
+ font-size: 0.7rem;
887
+ font-weight: bold;
888
+ }
889
+
890
+ /* Cart Modal Specific Styles */
891
+ .cart-item {
892
+ display: flex;
893
+ justify-content: space-between;
894
+ align-items: center;
895
+ padding: 15px 0;
896
+ border-bottom: 1px solid var(--border-color);
897
+ }
898
+ .cart-item:last-child {
899
+ border-bottom: none;
900
+ }
901
+ .cart-item img {
902
+ width: 60px;
903
+ height: 60px;
904
+ object-fit: cover;
905
+ border-radius: 8px;
906
+ margin-right: 15px;
907
+ border: 1px solid var(--border-color);
908
+ }
909
+ .cart-item-details {
910
+ flex-grow: 1;
911
+ margin-right: 15px;
912
+ }
913
+ .cart-item-details strong {
914
+ display: block;
915
+ margin-bottom: 3px;
916
+ }
917
+ .cart-item-details p {
918
+ font-size: 0.9rem;
919
+ color: #555;
920
+ margin-bottom: 0;
921
+ }
922
+ .cart-item-price {
923
+ font-weight: 600;
924
+ min-width: 80px; /* Ensure price alignment */
925
+ text-align: right;
926
+ }
927
+ .cart-actions {
928
+ display: flex;
929
+ justify-content: flex-end;
930
+ gap: 15px;
931
+ margin-top: 25px;
932
+ padding-top: 20px;
933
+ border-top: 1px solid var(--border-color);
934
+ }
935
+ #cartTotal {
936
+ font-size: 1.3rem;
937
+ }
938
+ .quantity-input, .color-select { /* Style for quantity/color modal */
939
+ width: 100%;
940
+ max-width: 200px; /* Adjust width */
941
+ padding: 10px;
942
+ border: 1px solid var(--border-color);
943
+ border-radius: 8px;
944
+ font-size: 1rem;
945
+ margin: 10px 0;
946
+ display: block; /* Ensure they take block space */
947
+ }
948
+ .modal-content .product-button { /* Reuse product button style */
949
+ margin-top: 15px;
950
+ }
951
+ </style>
952
+ </head>
953
+ <body>
954
+ <header class="header">
955
+ <a href="{{ url_for('landing') }}">
956
+ <img src="{{ logo_url }}" alt="Mebel Hause KG Logo" class="header-logo">
957
+ </a>
958
+ <nav>
959
+ <a href="{{ url_for('landing') }}#calculator">Калькулятор</a>
960
+ <a href="{{ url_for('catalog') }}" class="active">Каталог</a>
961
+ <a href="{{ url_for('landing') }}#about">О нас</a>
962
+ <a href="{{ url_for('landing') }}#contact">Контакты</a>
963
+ </nav>
964
+ </header>
965
+
966
+ <div class="container main-content">
967
+ <h1>Каталог готовых изделий</h1>
968
+ <p style="text-align: center; margin-bottom: 2rem;">Ознакомьтесь с нашими работами. Возможно, что-то из этого подойдет именно вам!</p>
969
+
970
+ <div class="filters-container">
971
+ <button class="category-filter active" data-category="all">Все категории</button>
972
+ {% for category in categories %}
973
+ <button class="category-filter" data-category="{{ category }}">{{ category }}</button>
974
+ {% endfor %}
975
+ </div>
976
+ <div class="search-container">
977
+ <input type="text" id="search-input" placeholder="Поиск по названию или описанию...">
978
+ </div>
979
+ <div class="products-grid" id="products-grid">
980
+ {% for product in products %}
981
+ <div class="product"
982
+ data-name="{{ product['name']|lower }}"
983
+ data-description="{{ product['description']|lower }}"
984
+ data-category="{{ product.get('category', 'Без категории') }}">
985
+ <div class="product-image">
986
+ {% if product.get('photos') and product['photos']|length > 0 %}
987
+ <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product['photos'][0] }}?download=true"
988
+ alt="{{ product['name'] }}"
989
+ loading="lazy">
990
+ {% else %}
991
+ <img src="https://via.placeholder.com/400x300?text=Нет+фото" alt="Нет фото">
992
+ {% endif %}
993
+ </div>
994
+ <div class="product-info">
995
+ <div class="product-info-top">
996
+ <h2>{{ product['name'] }}</h2>
997
+ <div class="product-price">{{ product['price'] }} сом</div>
998
+ <p class="product-description">{{ product['description'] }}</p>
999
+ </div>
1000
+ <div class="product-buttons">
1001
+ <button class="product-button" onclick="openModal({{ loop.index0 }})">Подробнее</button>
1002
+ <button class="product-button add-to-cart" onclick="openQuantityModal({{ loop.index0 }})">
1003
+ <i class="fas fa-cart-plus"></i> В корзину
1004
+ </button>
1005
+ </div>
1006
+ </div>
1007
+ </div>
1008
+ {% else %}
1009
+ <p>В каталоге пока нет товаров.</p>
1010
+ {% endfor %}
1011
+ </div>
1012
+ </div>
1013
+
1014
+ <div id="productModal" class="modal">
1015
+ <div class="modal-content">
1016
+ <span class="close" onclick="closeModal('productModal')">×</span>
1017
+ <div id="modalContent"></div>
1018
+ </div>
1019
+ </div>
1020
+
1021
+ <div id="quantityModal" class="modal">
1022
+ <div class="modal-content">
1023
+ <span class="close" onclick="closeModal('quantityModal')">×</span>
1024
+ <h2>Добавить в корзину</h2>
1025
+ <label for="quantityInput">Количество:</label>
1026
+ <input type="number" id="quantityInput" class="quantity-input" min="1" value="1">
1027
+ <div id="colorSelectorContainer">
1028
+ <label for="colorSelect">Цвет:</label>
1029
+ <select id="colorSelect" class="color-select"></select>
1030
+ </div>
1031
+ <button class="product-button add-to-cart" onclick="confirmAddToCart()">
1032
+ <i class="fas fa-check"></i> Добавить
1033
+ </button>
1034
+ </div>
1035
+ </div>
1036
+
1037
+ <div id="cartModal" class="modal">
1038
+ <div class="modal-content">
1039
+ <span class="close" onclick="closeModal('cartModal')">×</span>
1040
+ <h2><i class="fas fa-shopping-cart"></i> Ваша корзина</h2>
1041
+ <div id="cartContent"><p>Корзина пуста.</p></div>
1042
+ <div style="margin-top: 20px; text-align: right; font-size: 1.2rem; font-weight: 600;">
1043
+ <strong>Итого: <span id="cartTotal">0</span> сом</strong>
1044
+ </div>
1045
+ <div class="cart-actions">
1046
+ <button class="product-button delete-button" onclick="clearCart()">
1047
+ <i class="fas fa-trash"></i> Очистить
1048
+ </button>
1049
+ <button class="product-button add-to-cart" onclick="orderViaWhatsApp()">
1050
+ <i class="fab fa-whatsapp"></i> Заказать по WhatsApp
1051
+ </button>
1052
+ </div>
1053
+ </div>
1054
+ </div>
1055
+
1056
+ <button id="cart-button" onclick="openCartModal()">
1057
+ <i class="fas fa-shopping-cart"></i>
1058
+ <span id="cart-count">0</span>
1059
+ </button>
1060
+
1061
+ <a href="https://wa.me/{{ whatsapp_number }}" class="whatsapp-float" target="_blank">
1062
+ <i class="fab fa-whatsapp"></i>
1063
+ </a>
1064
+
1065
+ <footer class="footer">
1066
+ <p>© {{ current_year }} Mebel Hause KG. Все права защищены.</p>
1067
+ </footer>
1068
+
1069
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.js"></script>
1070
+ <script>
1071
+ const products = {{ products|tojson }};
1072
+ const repoId = "{{ repo_id }}";
1073
+ const whatsappNumber = "{{ whatsapp_number }}";
1074
+ let selectedProductIndex = null;
1075
+ let productModalSwiper = null;
1076
+
1077
+ function openModal(index) {
1078
+ loadProductDetails(index);
1079
+ document.getElementById('productModal').style.display = "block";
1080
+ }
1081
+
1082
+ function closeModal(modalId) {
1083
+ document.getElementById(modalId).style.display = "none";
1084
+ if (modalId === 'productModal' && productModalSwiper) {
1085
+ productModalSwiper.destroy(true, true);
1086
+ productModalSwiper = null;
1087
+ }
1088
+ }
1089
+
1090
+ function loadProductDetails(index) {
1091
+ fetch('/product/' + index)
1092
+ .then(response => {
1093
+ if (!response.ok) {
1094
+ throw new Error('Товар не найден');
1095
+ }
1096
+ return response.text();
1097
+ })
1098
+ .then(data => {
1099
+ document.getElementById('modalContent').innerHTML = data;
1100
+ initializeSwiper(); // Initialize Swiper after content is loaded
1101
+ })
1102
+ .catch(error => {
1103
+ console.error('Ошибка загрузки деталей товара:', error);
1104
+ document.getElementById('modalContent').innerHTML = '<p>Не удалось загрузить информацию о товаре.</p>';
1105
+ });
1106
+ }
1107
+
1108
+
1109
+ function initializeSwiper() {
1110
+ const swiperElement = document.querySelector('#productModal .swiper-container');
1111
+ if (swiperElement) {
1112
+ if (productModalSwiper) { // Destroy previous instance if exists
1113
+ productModalSwiper.destroy(true, true);
1114
+ }
1115
+ productModalSwiper = new Swiper(swiperElement, {
1116
+ slidesPerView: 1,
1117
+ spaceBetween: 20,
1118
+ loop: true, // Loop if more than one slide
1119
+ grabCursor: true,
1120
+ pagination: { el: '.swiper-pagination', clickable: true },
1121
+ navigation: { nextEl: '.swiper-button-next', prevEl: '.swiper-button-prev' },
1122
+ zoom: { maxRatio: 3, containerClass: 'swiper-zoom-container' },
1123
+ lazy: { loadPrevNext: true }, // Enable lazy loading
1124
+ });
1125
+ // Ensure loop works correctly only if there are multiple images
1126
+ if (productModalSwiper.slides.length <= 3) { // Swiper adds duplicates for loop
1127
+ productModalSwiper.params.loop = false;
1128
+ productModalSwiper.update();
1129
+ }
1130
+ } else {
1131
+ console.log("Swiper container not found in modal content.");
1132
+ }
1133
+ }
1134
+
1135
+
1136
+ function openQuantityModal(index) {
1137
+ selectedProductIndex = index;
1138
+ const product = products[index];
1139
+ if (!product) {
1140
+ console.error("Товар не найден по индексу:", index);
1141
+ alert("Ошибка: Товар не найден.");
1142
+ return;
1143
+ }
1144
+
1145
+ const colorSelect = document.getElementById('colorSelect');
1146
+ const colorContainer = document.getElementById('colorSelectorContainer');
1147
+ colorSelect.innerHTML = ''; // Clear previous options
1148
+
1149
+ if (product.colors && product.colors.length > 0) {
1150
+ product.colors.forEach(color => {
1151
+ const option = document.createElement('option');
1152
+ option.value = color;
1153
+ option.textContent = color;
1154
+ colorSelect.appendChild(option);
1155
+ });
1156
+ colorContainer.style.display = 'block'; // Show color selector
1157
+ } else {
1158
+ // If no colors, add a default placeholder and hide selector? Or just proceed?
1159
+ // Let's hide it if no colors are defined for this product
1160
+ colorContainer.style.display = 'none';
1161
+ }
1162
+
1163
+ document.getElementById('quantityInput').value = 1; // Reset quantity
1164
+ document.getElementById('quantityModal').style.display = 'block';
1165
+ }
1166
+
1167
+ function confirmAddToCart() {
1168
+ if (selectedProductIndex === null || !products[selectedProductIndex]) {
1169
+ console.error("Не выбран товар для добавления.");
1170
+ alert("Ошибка: Пожалуйста, выберите товар.");
1171
+ return;
1172
+ }
1173
+
1174
+ const quantity = parseInt(document.getElementById('quantityInput').value) || 1;
1175
+ const product = products[selectedProductIndex];
1176
+ const colorSelect = document.getElementById('colorSelect');
1177
+ // Use selected color if available, otherwise use a placeholder or null
1178
+ let color = 'N/A'; // Default if no colors
1179
+ if (product.colors && product.colors.length > 0) {
1180
+ color = colorSelect.value || product.colors[0]; // Get selected or first color
1181
+ }
1182
+
1183
+ if (quantity <= 0) {
1184
+ alert("Укажите количество больше 0");
1185
+ return;
1186
+ }
1187
+
1188
+ let cart = JSON.parse(localStorage.getItem('cart') || '[]');
1189
+ // Unique ID considering product name and color (if applicable)
1190
+ const cartItemId = product.colors && product.colors.length > 0 ? `${product.name}-${color}` : product.name;
1191
+ const existingItemIndex = cart.findIndex(item => item.id === cartItemId);
1192
+
1193
+ if (existingItemIndex > -1) {
1194
+ cart[existingItemIndex].quantity += quantity;
1195
+ } else {
1196
+ cart.push({
1197
+ id: cartItemId,
1198
+ name: product.name,
1199
+ price: product.price,
1200
+ photo: product.photos && product.photos.length > 0 ? product.photos[0] : '',
1201
+ quantity: quantity,
1202
+ color: color, // Store the selected color
1203
+ repoId: repoId // Store repoId for image URL generation later
1204
+ });
1205
+ }
1206
+
1207
+ localStorage.setItem('cart', JSON.stringify(cart));
1208
+ closeModal('quantityModal');
1209
+ updateCartButton();
1210
+ showNotification(`${product.name} добавлен в корзину!`);
1211
+ }
1212
+
1213
+ function showNotification(message) {
1214
+ const notification = document.createElement('div');
1215
+ notification.style.position = 'fixed';
1216
+ notification.style.bottom = '20px';
1217
+ notification.style.left = '50%';
1218
+ notification.style.transform = 'translateX(-50%)';
1219
+ notification.style.backgroundColor = 'var(--accent-wood)';
1220
+ notification.style.color = 'white';
1221
+ notification.style.padding = '10px 20px';
1222
+ notification.style.borderRadius = '8px';
1223
+ notification.style.zIndex = '1005';
1224
+ notification.style.opacity = '0';
1225
+ notification.style.transition = 'opacity 0.5s ease';
1226
+ notification.textContent = message;
1227
+ document.body.appendChild(notification);
1228
+
1229
+ // Fade in
1230
+ setTimeout(() => { notification.style.opacity = '1'; }, 10);
1231
+ // Fade out and remove
1232
+ setTimeout(() => {
1233
+ notification.style.opacity = '0';
1234
+ setTimeout(() => { document.body.removeChild(notification); }, 500);
1235
+ }, 3000); // Show for 3 seconds
1236
+ }
1237
+
1238
+
1239
+ function updateCartButton() {
1240
+ const cart = JSON.parse(localStorage.getItem('cart') || '[]');
1241
+ const cartButton = document.getElementById('cart-button');
1242
+ const cartCount = document.getElementById('cart-count');
1243
+ const totalItems = cart.reduce((sum, item) => sum + item.quantity, 0);
1244
+
1245
+ if (totalItems > 0) {
1246
+ cartButton.style.display = 'flex'; // Use flex to center icon
1247
+ cartCount.textContent = totalItems;
1248
+ cartCount.style.display = 'block';
1249
+ } else {
1250
+ cartButton.style.display = 'none';
1251
+ cartCount.style.display = 'none';
1252
+ }
1253
+ }
1254
+
1255
+ function openCartModal() {
1256
+ const cart = JSON.parse(localStorage.getItem('cart') || '[]');
1257
+ const cartContent = document.getElementById('cartContent');
1258
+ let total = 0;
1259
+
1260
+ if (cart.length === 0) {
1261
+ cartContent.innerHTML = '<p style="text-align: center; padding: 20px 0;">Ваша корзина пуста.</p>';
1262
+ document.getElementById('cartTotal').textContent = '0';
1263
+ } else {
1264
+ cartContent.innerHTML = cart.map(item => {
1265
+ const itemTotal = item.price * item.quantity;
1266
+ total += itemTotal;
1267
+ const photoUrl = item.photo ? `https://huggingface.co/datasets/${item.repoId}/resolve/main/photos/${item.photo}?download=true` : 'https://via.placeholder.com/60x60?text=N/A';
1268
+ return `
1269
+ <div class="cart-item">
1270
+ <img src="${photoUrl}" alt="${item.name}">
1271
+ <div class="cart-item-details">
1272
+ <strong>${item.name}</strong>
1273
+ <p>${item.quantity} шт. × ${item.price} сом ${item.color !== 'N/A' ? `(Цвет: ${item.color})` : ''}</p>
1274
+ </div>
1275
+ <span class="cart-item-price">${itemTotal} сом</span>
1276
+ <button onclick="removeFromCart('${item.id}')" style="background: none; border: none; color: #ef4444; cursor: pointer; font-size: 1.2rem; margin-left: 10px;" title="Удалить товар">×</button>
1277
+ </div>
1278
+ `;
1279
+ }).join('');
1280
+ document.getElementById('cartTotal').textContent = total;
1281
+ }
1282
+
1283
+ document.getElementById('cartModal').style.display = 'block';
1284
+ }
1285
+
1286
+ function removeFromCart(itemId) {
1287
+ let cart = JSON.parse(localStorage.getItem('cart') || '[]');
1288
+ cart = cart.filter(item => item.id !== itemId);
1289
+ localStorage.setItem('cart', JSON.stringify(cart));
1290
+ openCartModal(); // Refresh cart modal view
1291
+ updateCartButton(); // Update cart button count
1292
+ }
1293
+
1294
+ function orderViaWhatsApp() {
1295
+ const cart = JSON.parse(localStorage.getItem('cart') || '[]');
1296
+ if (cart.length === 0) {
1297
+ alert("Ваша корзина пуста!");
1298
+ return;
1299
+ }
1300
+ let total = 0;
1301
+ let orderText = "Здравствуйте! Хочу заказать следующие товары из каталога:\\n\\n";
1302
+ cart.forEach((item, index) => {
1303
+ const itemTotal = item.price * item.quantity;
1304
+ total += itemTotal;
1305
+ orderText += `${index + 1}. ${item.name}`;
1306
+ if (item.color !== 'N/A') {
1307
+ orderText += ` (Цвет: ${item.color})`;
1308
+ }
1309
+ orderText += ` - ${item.quantity} шт. x ${item.price} сом = ${itemTotal} сом\\n`;
1310
+ });
1311
+ orderText += `\\n*Итого: ${total} сом*`;
1312
+
1313
+ const encodedMessage = encodeURIComponent(orderText);
1314
+ // const whatsappUrl = `https://wa.me/${whatsappNumber}?text=${encodedMessage}`;
1315
+ const whatsappUrl = `https://api.whatsapp.com/send?phone=${whatsappNumber}&text=${encodedMessage}`;
1316
+
1317
+
1318
+ window.open(whatsappUrl, '_blank');
1319
+ // Optionally clear cart after sending? Or ask user?
1320
+ // clearCart();
1321
+ }
1322
+
1323
+ function clearCart() {
1324
+ if (confirm("Вы уверены, что хотите очистить корзину?")) {
1325
+ localStorage.removeItem('cart');
1326
+ closeModal('cartModal');
1327
+ updateCartButton();
1328
+ openCartModal(); // Refresh modal to show empty state
1329
+ }
1330
+ }
1331
+
1332
+ window.onclick = function(event) {
1333
+ const modals = document.querySelectorAll('.modal');
1334
+ modals.forEach(modal => {
1335
+ if (event.target == modal) {
1336
+ closeModal(modal.id);
1337
+ }
1338
+ });
1339
+ }
1340
+
1341
+ document.addEventListener('keydown', function(event) {
1342
+ if (event.key === "Escape") {
1343
+ const modals = document.querySelectorAll('.modal');
1344
+ modals.forEach(modal => {
1345
+ if (modal.style.display === "block") {
1346
+ closeModal(modal.id);
1347
+ }
1348
+ });
1349
+ }
1350
+ });
1351
+
1352
+ document.getElementById('search-input').addEventListener('input', filterProducts);
1353
+ document.querySelectorAll('.category-filter').forEach(filter => {
1354
+ filter.addEventListener('click', function() {
1355
+ document.querySelectorAll('.category-filter').forEach(f => f.classList.remove('active'));
1356
+ this.classList.add('active');
1357
+ filterProducts();
1358
+ });
1359
+ });
1360
+
1361
+ function filterProducts() {
1362
+ const searchTerm = document.getElementById('search-input').value.toLowerCase().trim();
1363
+ const activeCategory = document.querySelector('.category-filter.active').dataset.category;
1364
+ let hasVisibleProducts = false;
1365
+
1366
+ document.querySelectorAll('.product').forEach(product => {
1367
+ const name = product.getAttribute('data-name');
1368
+ const description = product.getAttribute('data-description');
1369
+ const category = product.getAttribute('data-category');
1370
+
1371
+ const matchesSearch = searchTerm === '' || name.includes(searchTerm) || description.includes(searchTerm);
1372
+ const matchesCategory = activeCategory === 'all' || category === activeCategory;
1373
+
1374
+ if (matchesSearch && matchesCategory) {
1375
+ product.style.display = 'flex'; // Use flex as it's the default for .product
1376
+ hasVisibleProducts = true;
1377
+ } else {
1378
+ product.style.display = 'none';
1379
+ }
1380
+ });
1381
+
1382
+ // Optional: Show a message if no products match
1383
+ const grid = document.getElementById('products-grid');
1384
+ let noMatchMsg = grid.querySelector('.no-match-message');
1385
+ if (!hasVisibleProducts) {
1386
+ if (!noMatchMsg) {
1387
+ noMatchMsg = document.createElement('p');
1388
+ noMatchMsg.className = 'no-match-message';
1389
+ noMatchMsg.textContent = 'Товары не найдены по вашему запросу.';
1390
+ noMatchMsg.style.textAlign = 'center';
1391
+ noMatchMsg.style.gridColumn = '1 / -1'; // Span all columns
1392
+ grid.appendChild(noMatchMsg);
1393
+ }
1394
+ } else {
1395
+ if (noMatchMsg) {
1396
+ grid.removeChild(noMatchMsg);
1397
+ }
1398
+ }
1399
+ }
1400
+
1401
+ // Initial setup
1402
+ document.addEventListener('DOMContentLoaded', () => {
1403
+ updateCartButton();
1404
+ filterProducts(); // Initial filter on page load
1405
+ });
1406
+ </script>
1407
+ </body>
1408
+ </html>
1409
+ '''
1410
+ return render_template_string(
1411
+ catalog_html,
1412
+ products=products,
1413
+ categories=categories,
1414
+ repo_id=REPO_ID,
1415
+ logo_url=LOGO_URL,
1416
+ whatsapp_number=WHATSAPP_NUMBER,
1417
+ current_year=datetime.now().year
1418
+ )
1419
+
1420
+
1421
+ # --- Product Detail Partial ---
1422
+ @app.route('/product/<int:index>')
1423
+ def product_detail(index):
1424
+ data = load_data()
1425
+ products = data.get('products', [])
1426
+ try:
1427
+ product = products[index]
1428
+ except IndexError:
1429
+ logging.warning(f"Попытка доступа к несуществующему продукту с индексом {index}")
1430
+ return "Товар не найден", 404
1431
+
1432
+ detail_html = '''
1433
+ <h2 style="font-size: 1.8rem; font-weight: 600; margin-bottom: 20px; text-align: center;">{{ product['name'] }}</h2>
1434
+
1435
+ <div class="swiper-container">
1436
+ <div class="swiper-wrapper">
1437
+ {% if product.get('photos') and product['photos']|length > 0 %}
1438
+ {% for photo in product['photos'] %}
1439
+ <div class="swiper-slide">
1440
+ <div class="swiper-zoom-container">
1441
+ <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo }}?download=true"
1442
+ alt="{{ product['name'] }} - Фото {{ loop.index }}"
1443
+ loading="lazy">
1444
+ </div>
1445
+ </div>
1446
+ {% endfor %}
1447
+ {% else %}
1448
+ <div class="swiper-slide">
1449
+ <img src="https://via.placeholder.com/450x350?text=Нет+фото" alt="Фото отсутствует">
1450
+ </div>
1451
+ {% endif %}
1452
+ </div>
1453
+ <!-- Add Pagination -->
1454
+ <div class="swiper-pagination"></div>
1455
+ <!-- Add Navigation -->
1456
+ <div class="swiper-button-next"></div>
1457
+ <div class="swiper-button-prev"></div>
1458
+ </div>
1459
+
1460
+ <div style="padding: 0 15px;"> <!-- Add padding around text details -->
1461
+ <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
1462
+ <p><strong>Цена:</strong> <span style="font-weight: 700; font-size: 1.2em; color: var(--accent-wood);">{{ product['price'] }} сом</span></p>
1463
+ <p><strong>Описание:</strong></p>
1464
+ <p style="white-space: pre-wrap;">{{ product['description'] }}</p> <!-- Preserve line breaks in description -->
1465
+ {% if product.get('colors') and product['colors']|length > 0 %}
1466
+ <p><strong>Доступные цвета:</strong> {{ product['colors']|join(', ') }}</p>
1467
+ {% endif %}
1468
+ </div>
1469
+ '''
1470
+ return render_template_string(detail_html, product=product, repo_id=REPO_ID)
1471
+
1472
+ # --- Admin Panel ---
1473
+ @app.route('/admin', methods=['GET', 'POST'])
1474
+ def admin():
1475
+ data = load_data()
1476
+ products = data.get('products', [])
1477
+ categories = data.get('categories', [])
1478
+
1479
+ message = None # To display success/error messages
1480
+
1481
+ if request.method == 'POST':
1482
+ action = request.form.get('action')
1483
+ logging.debug(f"Admin action received: {action}")
1484
+ logging.debug(f"Form data: {request.form}")
1485
+ logging.debug(f"Files data: {request.files}")
1486
+
1487
+ try:
1488
+ if action == 'add_category':
1489
+ category_name = request.form.get('category_name', '').strip()
1490
+ if category_name and category_name not in categories:
1491
+ categories.append(category_name)
1492
+ save_data(data)
1493
+ message = ("Категория добавлена", "success")
1494
+ elif not category_name:
1495
+ message = ("Название категории не может быть пустым", "error")
1496
+ else:
1497
+ message = ("Категория уже существует", "error")
1498
+ return redirect(url_for('admin', msg=message[0], type=message[1]))
1499
+
1500
+ elif action == 'delete_category':
1501
+ category_index_str = request.form.get('category_index')
1502
+ if category_index_str is not None:
1503
+ category_index = int(category_index_str)
1504
+ if 0 <= category_index < len(categories):
1505
+ deleted_category = categories.pop(category_index)
1506
+ # Update products using this category
1507
+ for product in products:
1508
+ if product.get('category') == deleted_category:
1509
+ product['category'] = 'Без категории' # Assign to default
1510
+ save_data(data)
1511
+ message = ("Категория удалена", "success")
1512
+ else:
1513
+ message = ("Неверный индекс категории", "error")
1514
+ else:
1515
+ message = ("Индекс категории не указан", "error")
1516
+ return redirect(url_for('admin', msg=message[0], type=message[1]))
1517
+
1518
+
1519
+ elif action == 'add' or action == 'edit':
1520
+ name = request.form.get('name', '').strip()
1521
+ price_str = request.form.get('price', '0').replace(',', '.')
1522
+ description = request.form.get('description', '').strip()
1523
+ category = request.form.get('category')
1524
+ photos_files = request.files.getlist('photos')
1525
+ # Get colors - handle empty strings and potential duplicates
1526
+ colors = [c.strip() for c in request.form.getlist('colors') if c.strip()]
1527
+ colors = sorted(list(set(colors))) # Remove duplicates and sort
1528
+
1529
+ logging.debug(f"Received colors: {request.form.getlist('colors')}")
1530
+ logging.debug(f"Processed colors: {colors}")
1531
+
1532
+ if not name or not description:
1533
+ message = ("Название и описание товара обязательны", "error")
1534
+ # Need to pass current state back if redirecting
1535
+ return redirect(url_for('admin', msg=message[0], type=message[1]))
1536
+ try:
1537
+ price = float(price_str)
1538
+ if price < 0: raise ValueError("Price cannot be negative")
1539
+ except ValueError:
1540
+ message = ("Некорректное значение цены", "error")
1541
+ return redirect(url_for('admin', msg=message[0], type=message[1]))
1542
+
1543
+ photos_list = []
1544
+ # Upload new photos if provided
1545
+ if photos_files and any(f.filename for f in photos_files):
1546
+ if not HF_TOKEN_WRITE:
1547
+ message = ("HF_TOKEN (write) не установлен. Невозможно загрузить фото.", "warning")
1548
+ # Decide if you want to proceed without photos or stop
1549
+ # return redirect(url_for('admin', msg=message[0], type=message[1]))
1550
+ else:
1551
+ api = HfApi()
1552
+ uploads_dir = 'uploads_temp' # Temporary local storage
1553
+ os.makedirs(uploads_dir, exist_ok=True)
1554
+
1555
+ for photo in photos_files[:10]: # Limit photos
1556
+ if photo and photo.filename:
1557
+ try:
1558
+ photo_filename = secure_filename(f"{datetime.now().strftime('%Y%m%d%H%M%S')}_{photo.filename}")
1559
+ temp_path = os.path.join(uploads_dir, photo_filename)
1560
+ photo.save(temp_path)
1561
+
1562
+ logging.info(f"Uploading photo {photo_filename} to HF...")
1563
+ api.upload_file(
1564
+ path_or_fileobj=temp_path,
1565
+ path_in_repo=f"photos/{photo_filename}",
1566
+ repo_id=REPO_ID,
1567
+ repo_type="dataset",
1568
+ token=HF_TOKEN_WRITE,
1569
+ commit_message=f"Фото для товара {name}"
1570
+ )
1571
+ photos_list.append(photo_filename)
1572
+ logging.info(f"Photo {photo_filename} uploaded successfully.")
1573
+ # Clean up local temp file
1574
+ if os.path.exists(temp_path):
1575
+ os.remove(temp_path)
1576
+ except Exception as e:
1577
+ logging.error(f"Ошибка загрузки фото {photo.filename}: {e}")
1578
+ message = (f"Ошибка загрузки фото {photo.filename}", "error")
1579
+ # Decide whether to stop or continue
1580
+
1581
+ # Clean up temp directory if empty
1582
+ if os.path.exists(uploads_dir) and not os.listdir(uploads_dir):
1583
+ os.rmdir(uploads_dir)
1584
+
1585
+
1586
+ if action == 'add':
1587
+ new_product = {
1588
+ 'name': name,
1589
+ 'price': price,
1590
+ 'description': description,
1591
+ 'category': category if category in categories else 'Без категории',
1592
+ 'photos': photos_list, # Only new photos for 'add'
1593
+ 'colors': colors
1594
+ }
1595
+ products.append(new_product)
1596
+ message = ("Товар успешно добавлен", "success")
1597
+
1598
+ elif action == 'edit':
1599
+ index_str = request.form.get('index')
1600
+ if index_str is not None:
1601
+ index = int(index_str)
1602
+ if 0 <= index < len(products):
1603
+ # Update fields
1604
+ products[index]['name'] = name
1605
+ products[index]['price'] = price
1606
+ products[index]['description'] = description
1607
+ products[index]['category'] = category if category in categories else 'Без категории'
1608
+ products[index]['colors'] = colors
1609
+
1610
+ # If new photos were uploaded, replace the old list. Otherwise, keep existing photos.
1611
+ if photos_list:
1612
+ # Optionally delete old photos from HF here if needed (more complex)
1613
+ products[index]['photos'] = photos_list
1614
+ # If no new photos uploaded, 'photos_list' is empty,
1615
+ # and we don't touch products[index]['photos']
1616
+
1617
+ message = ("Товар успешно обновлен", "success")
1618
+ else:
1619
+ message = ("Неверный индекс товара для редактирования", "error")
1620
+ else:
1621
+ message = ("Индекс товара для редактирования не указан", "error")
1622
+
1623
+
1624
+ elif action == 'delete':
1625
+ index_str = request.form.get('index')
1626
+ if index_str is not None:
1627
+ index = int(index_str)
1628
+ if 0 <= index < len(products):
1629
+ # Optionally delete photos from HF here (more complex)
1630
+ del products[index]
1631
+ message = ("Товар удален", "success")
1632
+ else:
1633
+ message = ("Неверный индекс товара для удаления", "error")
1634
+ else:
1635
+ message = ("Индекс товара для удаления не указан", "error")
1636
+
1637
+
1638
+ # Save changes if any action potentially modified data (except delete errors)
1639
+ if message and message[1] != "error": # Save on success or warning
1640
+ save_data(data)
1641
+ # Redirect only after processing and potential save
1642
+ return redirect(url_for('admin', msg=message[0] if message else None, type=message[1] if message else None))
1643
+
1644
+ except Exception as e:
1645
+ logging.error(f"Ошибка в админ панели ({action}): {e}", exc_info=True)
1646
+ message = (f"Произошла внутренняя ошибка: {e}", "error")
1647
+ return redirect(url_for('admin', msg=message[0], type=message[1]))
1648
+
1649
+
1650
+ # --- Admin HTML ---
1651
+ flash_message = request.args.get('msg')
1652
+ flash_type = request.args.get('type', 'info') # Default type
1653
+
1654
+ admin_html = '''
1655
+ <!DOCTYPE html>
1656
+ <html lang="ru">
1657
+ <head>
1658
+ <meta charset="UTF-8">
1659
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1660
+ <title>Админ-панель - Mebel Hause KG</title>
1661
+ ''' + GLOBAL_STYLES + '''
1662
+ <style>
1663
+ /* Admin specific styles */
1664
+ .admin-container {
1665
+ padding-top: 20px;
1666
+ }
1667
+ .admin-section {
1668
+ margin-bottom: 40px;
1669
+ }
1670
+ .product-list, .category-list {
1671
+ display: grid;
1672
+ gap: 20px;
1673
+ margin-top: 20px;
1674
+ }
1675
+ .product-item, .category-item {
1676
+ background: var(--card-bg);
1677
+ padding: 20px;
1678
+ border-radius: 15px;
1679
+ box-shadow: 0 4px 15px rgba(0,0,0,0.08);
1680
+ border: 1px solid var(--border-color);
1681
+ }
1682
+ .product-item h3, .category-item h3 {
1683
+ margin-top: 0;
1684
+ margin-bottom: 10px;
1685
+ font-size: 1.3rem;
1686
+ border-bottom: 1px solid var(--border-color);
1687
+ padding-bottom: 8px;
1688
+ }
1689
+ .product-item p {
1690
+ margin-bottom: 8px;
1691
+ font-size: 0.95rem;
1692
+ }
1693
+ .edit-form {
1694
+ margin-top: 20px;
1695
+ padding: 20px;
1696
+ background: #f9f9f7; /* Slightly off-white */
1697
+ border: 1px solid var(--border-color);
1698
+ border-radius: 10px;
1699
+ }
1700
+ details summary {
1701
+ cursor: pointer;
1702
+ font-weight: 500;
1703
+ color: var(--link-color);
1704
+ margin: 15px 0;
1705
+ display: inline-block; /* Fit content */
1706
+ }
1707
+ .product-photos-admin {
1708
+ display: flex;
1709
+ flex-wrap: wrap;
1710
+ gap: 10px;
1711
+ margin-top: 10px;
1712
+ }
1713
+ .product-photos-admin img {
1714
+ max-width: 100px;
1715
+ max-height: 100px;
1716
+ object-fit: cover;
1717
+ border-radius: 8px;
1718
+ border: 1px solid var(--border-color);
1719
+ }
1720
+ .action-buttons form {
1721
+ display: inline-block;
1722
+ margin-right: 10px;
1723
+ }
1724
+ .action-buttons button {
1725
+ padding: 8px 15px;
1726
+ font-size: 0.9rem;
1727
+ }
1728
+
1729
+ /* Flash Messages */
1730
+ .flash-message {
1731
+ padding: 15px;
1732
+ margin-bottom: 20px;
1733
+ border-radius: 8px;
1734
+ font-weight: 500;
1735
+ }
1736
+ .flash-message.success {
1737
+ background-color: #d1e7dd; /* Light green */
1738
+ color: #0f5132; /* Dark green */
1739
+ border: 1px solid #badbcc;
1740
+ }
1741
+ .flash-message.error {
1742
+ background-color: #f8d7da; /* Light red */
1743
+ color: #842029; /* Dark red */
1744
+ border: 1px solid #f5c2c7;
1745
+ }
1746
+ .flash-message.warning {
1747
+ background-color: #fff3cd; /* Light yellow */
1748
+ color: #664d03; /* Dark yellow */
1749
+ border: 1px solid #ffecb5;
1750
+ }
1751
+ .flash-message.info {
1752
+ background-color: #cff4fc; /* Light blue */
1753
+ color: #055160; /* Dark blue */
1754
+ border: 1px solid #b6effb;
1755
+ }
1756
+ </style>
1757
+ </head>
1758
+ <body>
1759
+ <header class="header">
1760
+ <a href="{{ url_for('landing') }}">
1761
+ <img src="{{ logo_url }}" alt="Mebel Hause KG Logo" class="header-logo">
1762
+ </a>
1763
+ <nav>
1764
+ <a href="{{ url_for('landing') }}">Главная</a>
1765
+ <a href="{{ url_for('catalog') }}">Каталог</a>
1766
+ <a href="{{ url_for('admin') }}" class="active">Админ</a>
1767
+ </nav>
1768
+ </header>
1769
+
1770
+ <div class="container admin-container">
1771
+ <h1>Админ-панель</h1>
1772
+
1773
+ {% if flash_message %}
1774
+ <div class="flash-message {{ flash_type }}">
1775
+ {{ flash_message }}
1776
+ </div>
1777
+ {% endif %}
1778
+
1779
+ <section class="admin-section form-section">
1780
+ <h2>Добавить новый товар</h2>
1781
+ <form method="POST" enctype="multipart/form-data">
1782
+ <input type="hidden" name="action" value="add">
1783
+ <label for="add-name">Название товара:</label>
1784
+ <input type="text" id="add-name" name="name" required>
1785
+
1786
+ <label for="add-price">Цена (сом):</label>
1787
+ <input type="number" id="add-price" name="price" step="0.01" min="0" required>
1788
+
1789
+ <label for="add-description">Описание:</label>
1790
+ <textarea id="add-description" name="description" rows="4" required></textarea>
1791
+
1792
+ <label for="add-category">Категория:</label>
1793
+ <select id="add-category" name="category">
1794
+ <option value="Без категории">Без категории</option>
1795
+ {% for category in categories %}
1796
+ <option value="{{ category }}">{{ category }}</option>
1797
+ {% endfor %}
1798
+ </select>
1799
+
1800
+ <label for="add-photos">Фотографии (до 10 шт.):</label>
1801
+ <input type="file" id="add-photos" name="photos" accept="image/*" multiple>
1802
+
1803
+ <label>Цвета:</label>
1804
+ <div id="add-color-inputs">
1805
+ <div class="color-input-group">
1806
+ <input type="text" name="colors" placeholder="Например: Красный">
1807
+ <button type="button" class="remove-color-btn" onclick="removeColorInput(this)" style="display: none;">×</button>
1808
+ </div>
1809
+ </div>
1810
+ <button type="button" class="button-link add-color-btn" onclick="addColorInput('add-color-inputs')">Добавить цвет</button>
1811
+
1812
+ <button type="submit" class="form-button">Добавить товар</button>
1813
+ </form>
1814
+ </section>
1815
+
1816
+ <section class="admin-section">
1817
+ <h2>Управление категориями</h2>
1818
+ <div class="form-section">
1819
+ <form method="POST">
1820
+ <input type="hidden" name="action" value="add_category">
1821
+ <label for="category-name">Название новой категории:</label>
1822
+ <input type="text" id="category-name" name="category_name" required>
1823
+ <button type="submit" class="form-button">Добавить категорию</button>
1824
+ </form>
1825
+ </div>
1826
+
1827
+ <h3>Список категорий</h3>
1828
+ {% if categories %}
1829
+ <div class="category-list">
1830
+ {% for category in categories %}
1831
+ <div class="category-item">
1832
+ <h3>{{ category }}</h3>
1833
+ {% if category != 'Без категории' %}
1834
+ <form method="POST" style="display: inline;">
1835
+ <input type="hidden" name="action" value="delete_category">
1836
+ <input type="hidden" name="category_index" value="{{ loop.index0 }}">
1837
+ <button type="submit" class="button-link delete-button" onclick="return confirm('Удалить категорию {{ category }}? Товары будут перемещены в категорию Без категории.');">Удалить</button>
1838
+ </form>
1839
+ {% else %}
1840
+ <p><i>(Системная категория, не удаляется)</i></p>
1841
+ {% endif %}
1842
+ </div>
1843
+ {% endfor %}
1844
+ </div>
1845
+ {% else %}
1846
+ <p>Категорий пока нет.</p>
1847
+ {% endif %}
1848
+ </section>
1849
+
1850
+ <section class="admin-section">
1851
+ <h2>Управление базой данных</h2>
1852
+ <div class="action-buttons">
1853
+ <form method="POST" action="{{ url_for('backup') }}" style="display: inline;">
1854
+ <button type="submit" class="button-link">Создать резервную копию на HF</button>
1855
+ </form>
1856
+ <form method="GET" action="{{ url_for('download') }}" style="display: inline;">
1857
+ <button type="submit" class="button-link">Скачать базу данных (JSON)</button>
1858
+ </form>
1859
+ </div>
1860
+ </section>
1861
+
1862
+
1863
+ <section class="admin-section">
1864
+ <h2>Список товаров</h2>
1865
+ {% if products %}
1866
+ <div class="product-list">
1867
+ {% for product in products %}
1868
+ <div class="product-item">
1869
+ <h3>{{ product['name'] }}</h3>
1870
+ <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
1871
+ <p><strong>Цена:</strong> {{ product['price'] }} сом</p>
1872
+ <p><strong>Описание:</strong> {{ product['description'][:100] }}{% if product['description']|length > 100 %}...{% endif %}</p>
1873
+ <p><strong>Цвета:</strong> {{ product.get('colors', [])|join(', ') if product.get('colors') else 'Не указаны' }}</p>
1874
+
1875
+ {% if product.get('photos') and product['photos']|length > 0 %}
1876
+ <p><strong>Фотографии:</strong></p>
1877
+ <div class="product-photos-admin">
1878
+ {% for photo in product['photos'] %}
1879
+ <a href="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo }}?download=true" target="_blank">
1880
+ <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo }}?download=true"
1881
+ alt="{{ product['name'] }} - Фото {{ loop.index }}" loading="lazy">
1882
+ </a>
1883
+ {% endfor %}
1884
+ </div>
1885
+ {% else %}
1886
+ <p><strong>Фотографии:</strong> Нет</p>
1887
+ {% endif %}
1888
+
1889
+ <details>
1890
+ <summary>Редактировать товар</summary>
1891
+ <form method="POST" enctype="multipart/form-data" class="edit-form">
1892
+ <input type="hidden" name="action" value="edit">
1893
+ <input type="hidden" name="index" value="{{ loop.index0 }}">
1894
+
1895
+ <label for="edit-name-{{ loop.index0 }}">Название:</label>
1896
+ <input type="text" id="edit-name-{{ loop.index0 }}" name="name" value="{{ product['name'] }}" required>
1897
+
1898
+ <label for="edit-price-{{ loop.index0 }}">Цена:</label>
1899
+ <input type="number" id="edit-price-{{ loop.index0 }}" name="price" step="0.01" min="0" value="{{ product['price'] }}" required>
1900
+
1901
+ <label for="edit-description-{{ loop.index0 }}">Описание:</label>
1902
+ <textarea id="edit-description-{{ loop.index0 }}" name="description" rows="4" required>{{ product['description'] }}</textarea>
1903
+
1904
+ <label for="edit-category-{{ loop.index0 }}">Категория:</label>
1905
+ <select id="edit-category-{{ loop.index0 }}" name="category">
1906
+ <option value="Без категории" {% if product.get('category', 'Без категории') == 'Без категории' %}selected{% endif %}>Без категории</option>
1907
+ {% for cat in categories %}
1908
+ <option value="{{ cat }}" {% if product.get('category') == cat %}selected{% endif %}>{{ cat }}</option>
1909
+ {% endfor %}
1910
+ </select>
1911
+
1912
+ <label for="edit-photos-{{ loop.index0 }}">Заменить фотографии (до 10, старые будут удалены при загрузке новых):</label>
1913
+ <input type="file" id="edit-photos-{{ loop.index0 }}" name="photos" accept="image/*" multiple>
1914
+
1915
+ <label>Цвета:</label>
1916
+ <div id="edit-color-inputs-{{ loop.index0 }}">
1917
+ {% if product.get('colors') %}
1918
+ {% for color in product.get('colors', []) %}
1919
+ <div class="color-input-group">
1920
+ <input type="text" name="colors" value="{{ color }}">
1921
+ <button type="button" class="remove-color-btn" onclick="removeColorInput(this)">×</button>
1922
+ </div>
1923
+ {% endfor %}
1924
+ {% else %}
1925
+ <div class="color-input-group">
1926
+ <input type="text" name="colors" placeholder="Например: Синий">
1927
+ <button type="button" class="remove-color-btn" onclick="removeColorInput(this)" style="display:none;">×</button>
1928
+ </div>
1929
+ {% endif %}
1930
+ {% if not product.get('colors') %} {# Ensure at least one input exists even if no colors are set #}
1931
+ <div class="color-input-group" style="display:none;"> {# Hidden template if no colors initially #}
1932
+ <input type="text" name="colors" placeholder="Например: Синий">
1933
+ <button type="button" class="remove-color-btn" onclick="removeColorInput(this)">×</button>
1934
+ </div>
1935
+ {% endif %}
1936
+ </div>
1937
+ <button type="button" class="button-link add-color-btn" onclick="addColorInput('edit-color-inputs-{{ loop.index0 }}')">Добавить цвет</button>
1938
+
1939
+ <button type="submit" class="form-button">Сохранить изменения</button>
1940
+ </form>
1941
+ </details>
1942
+
1943
+ <div class="action-buttons">
1944
+ <form method="POST" onsubmit="return confirm('Вы уверены, что хотите удалить товар {{ product['name'] }}?');">
1945
+ <input type="hidden" name="action" value="delete">
1946
+ <input type="hidden" name="index" value="{{ loop.index0 }}">
1947
+ <button type="submit" class="button-link delete-button">Удалить товар</button>
1948
+ </form>
1949
+ </div>
1950
+ </div>
1951
+ {% endfor %}
1952
+ </div>
1953
+ {% else %}
1954
+ <p>В базе данных пока нет товаров.</p>
1955
+ {% endif %}
1956
+ </section>
1957
+
1958
+ </div>
1959
+
1960
+ <footer class="footer">
1961
+ <p>© {{ current_year }} Mebel Hause KG. Админ-панель.</p>
1962
+ </footer>
1963
+
1964
+ <script>
1965
+ function addColorInput(containerId) {
1966
+ const container = document.getElementById(containerId);
1967
+ if (!container) return;
1968
+
1969
+ // Find a template or the last input group to clone
1970
+ const templateGroup = container.querySelector('.color-input-group:last-child');
1971
+ const newGroup = templateGroup ? templateGroup.cloneNode(true) : document.createElement('div');
1972
+ newGroup.className = 'color-input-group'; // Ensure class name
1973
+
1974
+ if (!templateGroup) { // If container was empty, create structure
1975
+ newGroup.innerHTML = '<input type="text" name="colors" placeholder="Например: Цвет"><button type="button" class="remove-color-btn" onclick="removeColorInput(this)">×</button>';
1976
+ } else {
1977
+ const input = newGroup.querySelector('input[name="colors"]');
1978
+ if (input) input.value = ''; // Clear the value
1979
+ const removeBtn = newGroup.querySelector('.remove-color-btn');
1980
+ if (removeBtn) removeBtn.style.display = 'inline-block'; // Ensure remove button is visible
1981
+ }
1982
+
1983
+ newGroup.style.display = 'flex'; // Ensure the new group is visible
1984
+ container.appendChild(newGroup);
1985
+
1986
+ // Show remove button on previous last item if it was hidden
1987
+ const allGroups = container.querySelectorAll('.color-input-group');
1988
+ if (allGroups.length > 1) {
1989
+ const secondLastGroup = allGroups[allGroups.length - 2];
1990
+ const prevRemoveBtn = secondLastGroup.querySelector('.remove-color-btn');
1991
+ if (prevRemoveBtn) prevRemoveBtn.style.display = 'inline-block';
1992
+ }
1993
+ // Always hide remove button if only one input remains
1994
+ updateRemoveButtons(containerId);
1995
+ }
1996
+
1997
+ function removeColorInput(button) {
1998
+ const container = button.closest('[id^="color-inputs"], [id^="edit-color-inputs"]');
1999
+ const groupToRemove = button.closest('.color-input-group');
2000
+ if (container && groupToRemove) {
2001
+ // Only remove if there's more than one group
2002
+ if (container.querySelectorAll('.color-input-group').length > 1) {
2003
+ groupToRemove.remove();
2004
+ } else {
2005
+ // If it's the last one, just clear the input value
2006
+ const input = groupToRemove.querySelector('input[name="colors"]');
2007
+ if (input) input.value = '';
2008
+ alert("Должно быть хотя бы одно поле для цвета. Очищено.");
2009
+ }
2010
+ updateRemoveButtons(container.id); // Update visibility after removal
2011
+ }
2012
+ }
2013
+
2014
+ function updateRemoveButtons(containerId) {
2015
+ const container = document.getElementById(containerId);
2016
+ if (!container) return;
2017
+ const allGroups = container.querySelectorAll('.color-input-group');
2018
+ allGroups.forEach((group, index) => {
2019
+ const removeBtn = group.querySelector('.remove-color-btn');
2020
+ if (removeBtn) {
2021
+ // Show remove button only if there is more than one group
2022
+ removeBtn.style.display = (allGroups.length > 1) ? 'inline-block' : 'none';
2023
+ }
2024
+ });
2025
+ }
2026
+
2027
+ // Initial call to set up remove buttons correctly on page load for edit forms
2028
+ document.addEventListener('DOMContentLoaded', () => {
2029
+ const editColorContainers = document.querySelectorAll('[id^="edit-color-inputs-"]');
2030
+ editColorContainers.forEach(container => {
2031
+ updateRemoveButtons(container.id);
2032
+ });
2033
+ // Also for the add form
2034
+ updateRemoveButtons('add-color-inputs');
2035
+ });
2036
+
2037
+ </script>
2038
+ </body>
2039
+ </html>
2040
+ '''
2041
+ return render_template_string(
2042
+ admin_html,
2043
+ products=products,
2044
+ categories=categories,
2045
+ repo_id=REPO_ID,
2046
+ logo_url=LOGO_URL,
2047
+ flash_message=flash_message,
2048
+ flash_type=flash_type,
2049
+ current_year=datetime.now().year
2050
+ )
2051
+
2052
+ @app.route('/backup', methods=['POST'])
2053
+ def backup():
2054
+ try:
2055
+ upload_db_to_hf()
2056
+ # Redirect back with success message
2057
+ return redirect(url_for('admin', msg="Резервное копирование на Hugging Face запущено.", type="success"))
2058
+ except Exception as e:
2059
+ logging.error(f"Ошибка при ручном резервном копировании: {e}")
2060
+ return redirect(url_for('admin', msg=f"Ошибка резервного копирования: {e}", type="error"))
2061
+
2062
+
2063
+ @app.route('/download', methods=['GET'])
2064
+ def download():
2065
+ try:
2066
+ # Optionally trigger a download from HF first to ensure local is up-to-date
2067
+ # download_db_from_hf() # Uncomment if you want to force sync before download
2068
+ if os.path.exists(DATA_FILE):
2069
+ return send_file(DATA_FILE, as_attachment=True, download_name='mebelhause_database.json')
2070
+ else:
2071
+ return redirect(url_for('admin', msg="Локальный файл базы данных не найден.", type="error"))
2072
+ except Exception as e:
2073
+ logging.error(f"Ошибка при скачивании файла базы данных: {e}")
2074
+ return redirect(url_for('admin', msg=f"Ошибка скачивания файла: {e}", type="error"))
2075
+
2076
+
2077
+ if __name__ == '__main__':
2078
+ # Ensure data file exists on start, try loading/downloading
2079
+ try:
2080
+ initial_data = load_data()
2081
+ # If load_data created an empty structure due to errors, save it.
2082
+ if not os.path.exists(DATA_FILE) or os.path.getsize(DATA_FILE) == 0:
2083
+ save_data(initial_data)
2084
+ except Exception as e:
2085
+ logging.error(f"Критическая ошибка при инициализации базы данных: {e}")
2086
+ # Consider exiting if the DB is crucial and cannot be loaded/created
2087
+ # exit(1)
2088
+
2089
+ # Start periodic backup in a separate thread
2090
+ if HF_TOKEN_WRITE and HF_TOKEN_READ: # Only run backup if tokens are set
2091
+ backup_thread = threading.Thread(target=periodic_backup, daemon=True)
2092
+ backup_thread.start()
2093
+ else:
2094
+ logging.warning("Токены Hugging Face (WRITE или READ) не установлены. Периодическое резервное копирование отключено.")
2095
+
2096
+ # Run Flask App
2097
+ # Use 'waitress' for production instead of Flask's built-in server
2098
+ # from waitress import serve
2099
+ # serve(app, host='0.0.0.0', port=7860)
2100
+ # For development:
2101
+ app.run(debug=False, host='0.0.0.0', port=7860) # Set debug=False for production/waitress