Kgshop commited on
Commit
9f1ad37
·
verified ·
1 Parent(s): 01d0b80

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +1412 -0
app.py ADDED
@@ -0,0 +1,1412 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template_string, request, redirect, url_for, send_file
2
+ import json
3
+ import os
4
+ import logging
5
+ import threading
6
+ import time
7
+ from datetime import datetime
8
+ from huggingface_hub import HfApi, hf_hub_download
9
+ from huggingface_hub.utils import RepositoryNotFoundError
10
+ from werkzeug.utils import secure_filename
11
+
12
+ app = Flask(__name__)
13
+ DATA_FILE = 'data_udeda2.json'
14
+
15
+ # Настройки Hugging Face
16
+ REPO_ID = "Kgshop/clients" # Замените на ваш ID репозитория
17
+ HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
18
+ HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
19
+
20
+ # Ссылка на логотип
21
+ LOGO_URL = "https://huggingface.co/spaces/Udeda/tkani/resolve/main/Picsart_25-03-20_11-33-51-169.jpg" # Замените на вашу ссылку
22
+
23
+ # Настройка логирования
24
+ logging.basicConfig(level=logging.DEBUG)
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 isinstance(data, dict) and 'products' in data:
33
+ for product in data['products']:
34
+ if 'roll_meters' not in product:
35
+ product['roll_meters'] = 0
36
+ if not isinstance(data, dict) or 'products' not in data or 'categories' not in data:
37
+ return {'products': [], 'categories': [] if not isinstance(data, list) else data}
38
+ return data
39
+ except FileNotFoundError:
40
+ logging.warning("Локальный файл базы данных не найден после скачивания.")
41
+ return {'products': [], 'categories': []}
42
+ except json.JSONDecodeError:
43
+ logging.error("Ошибка: Невозможно декодировать JSON файл.")
44
+ return {'products': [], 'categories': []}
45
+ except RepositoryNotFoundError:
46
+ logging.error("Репозиторий не найден. Создание локальной базы данных.")
47
+ return {'products': [], 'categories': []}
48
+ except Exception as e:
49
+ logging.error(f"Произошла ошибка при загрузке данных: {e}")
50
+ return {'products': [], 'categories': []}
51
+
52
+ def save_data(data):
53
+ try:
54
+ with open(DATA_FILE, 'w', encoding='utf-8') as file:
55
+ json.dump(data, file, ensure_ascii=False, indent=4)
56
+ logging.info("Данные успешно сохранены в JSON")
57
+ upload_db_to_hf()
58
+ except Exception as e:
59
+ logging.error(f"Ошибка при сохранении данных: {e}")
60
+ raise
61
+
62
+ def upload_db_to_hf():
63
+ try:
64
+ api = HfApi()
65
+ api.upload_file(
66
+ path_or_fileobj=DATA_FILE,
67
+ path_in_repo=DATA_FILE,
68
+ repo_id=REPO_ID,
69
+ repo_type="dataset",
70
+ token=HF_TOKEN_WRITE,
71
+ commit_message=f"Автоматическое резервное копирование базы данных {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
72
+ )
73
+ logging.info("Резервная копия JSON базы успешно загружена на Hugging Face.")
74
+ except Exception as e:
75
+ logging.error(f"Ошибка при загрузке резервной копии: {e}")
76
+
77
+ def download_db_from_hf():
78
+ try:
79
+ hf_hub_download(
80
+ repo_id=REPO_ID,
81
+ filename=DATA_FILE,
82
+ repo_type="dataset",
83
+ token=HF_TOKEN_READ,
84
+ local_dir=".",
85
+ local_dir_use_symlinks=False
86
+ )
87
+ logging.info("JSON база успешно скачана из Hugging Face.")
88
+ except RepositoryNotFoundError as e:
89
+ logging.error(f"Репозиторий не найден: {e}")
90
+ raise
91
+ except Exception as e:
92
+ logging.error(f"Ошибка при скачивании JSON базы: {e}")
93
+ raise
94
+
95
+ def periodic_backup():
96
+ while True:
97
+ upload_db_to_hf()
98
+ time.sleep(800)
99
+
100
+ @app.route('/')
101
+ def catalog():
102
+ data = load_data()
103
+ products = data['products']
104
+ categories = data['categories']
105
+
106
+ catalog_html = '''
107
+ <!DOCTYPE html>
108
+ <html lang="ru">
109
+ <head>
110
+ <meta charset="UTF-8">
111
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
112
+ <title>У Деда - ткани оптом</title>
113
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
114
+ <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
115
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.css">
116
+ <style>
117
+ * {
118
+ margin: 0;
119
+ padding: 0;
120
+ box-sizing: border-box;
121
+ }
122
+ body {
123
+ font-family: 'Poppins', sans-serif;
124
+ background: linear-gradient(135deg, #f0f2f5, #e9ecef);
125
+ color: #2d3748;
126
+ line-height: 1.6;
127
+ transition: background 0.3s, color 0.3s;
128
+ }
129
+ body.dark-mode {
130
+ background: linear-gradient(135deg, #1a202c, #2d3748);
131
+ color: #e2e8f0;
132
+ }
133
+ .container {
134
+ max-width: 1300px;
135
+ margin: 0 auto;
136
+ padding: 20px;
137
+ }
138
+ .header {
139
+ display: flex;
140
+ justify-content: space-between;
141
+ align-items: center;
142
+ padding: 15px 0;
143
+ border-bottom: 1px solid #e2e8f0;
144
+ }
145
+ .header-logo-container {
146
+ cursor: pointer;
147
+ position: relative;
148
+ display: inline-block;
149
+ }
150
+ .header-logo {
151
+ width: 60px;
152
+ height: 60px;
153
+ border-radius: 50%;
154
+ object-fit: cover;
155
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
156
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
157
+ }
158
+ .header-logo:hover {
159
+ transform: scale(1.1);
160
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
161
+ }
162
+ .header h1 {
163
+ font-size: 1.5rem;
164
+ font-weight: 600;
165
+ margin-left: 15px;
166
+ }
167
+ .theme-toggle {
168
+ background: none;
169
+ border: none;
170
+ font-size: 1.5rem;
171
+ cursor: pointer;
172
+ color: #4a5568;
173
+ transition: color 0.3s ease;
174
+ }
175
+ .theme-toggle:hover {
176
+ color: #3b82f6;
177
+ }
178
+ .filters-container {
179
+ margin: 20px 0;
180
+ display: flex;
181
+ flex-wrap: wrap;
182
+ gap: 10px;
183
+ justify-content: center;
184
+ }
185
+ .search-container {
186
+ margin: 20px 0;
187
+ text-align: center;
188
+ }
189
+ #search-input {
190
+ width: 90%;
191
+ max-width: 600px;
192
+ padding: 12px 18px;
193
+ font-size: 1rem;
194
+ border: 1px solid #e2e8f0;
195
+ border-radius: 8px;
196
+ outline: none;
197
+ box-shadow: 0 2px 5px rgba(0,0,0,0.05);
198
+ transition: all 0.3s ease;
199
+ }
200
+ #search-input:focus {
201
+ border-color: #3b82f6;
202
+ box-shadow: 0 4px 15px rgba(59, 130, 246, 0.2);
203
+ }
204
+ .category-filter {
205
+ padding: 8px 16px;
206
+ border: 1px solid #e2e8f0;
207
+ border-radius: 8px;
208
+ background-color: #fff;
209
+ cursor: pointer;
210
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
211
+ font-size: 0.9rem;
212
+ font-weight: 400;
213
+ }
214
+ .category-filter.active, .category-filter:hover {
215
+ background-color: #3b82f6;
216
+ color: white;
217
+ border-color: #3b82f6;
218
+ box-shadow: 0 2px 10px rgba(59, 130, 246, 0.3);
219
+ }
220
+ .products-grid {
221
+ display: grid;
222
+ grid-template-columns: repeat(2, minmax(200px, 1fr));
223
+ gap: 15px;
224
+ padding: 10px;
225
+ }
226
+ .product {
227
+ background: #fff;
228
+ border-radius: 15px;
229
+ padding: 15px;
230
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
231
+ transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.3s ease;
232
+ overflow: hidden;
233
+ cursor: pointer;
234
+ }
235
+ body.dark-mode .product {
236
+ background: #2d3748;
237
+ color: #fff;
238
+ }
239
+ .product:hover {
240
+ transform: translateY(-5px) scale(1.02);
241
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
242
+ }
243
+ .product-image {
244
+ width: 100%;
245
+ aspect-ratio: 1;
246
+ background-color: #fff;
247
+ border-radius: 10px;
248
+ overflow: hidden;
249
+ display: flex;
250
+ justify-content: center;
251
+ align-items: center;
252
+ }
253
+ .product-image img {
254
+ max-width: 100%;
255
+ max-height: 100%;
256
+ object-fit: contain;
257
+ transition: transform 0.3s ease;
258
+ }
259
+ .product-image img:hover {
260
+ transform: scale(1.1);
261
+ }
262
+ .product h2 {
263
+ font-size: 1rem;
264
+ font-weight: 600;
265
+ margin: 10px 0;
266
+ text-align: center;
267
+ white-space: nowrap;
268
+ overflow: hidden;
269
+ text-overflow: ellipsis;
270
+ }
271
+ .product-price {
272
+ font-size: 1.1rem;
273
+ color: #ef4444;
274
+ font-weight: 700;
275
+ text-align: center;
276
+ margin: 5px 0;
277
+ }
278
+ .product-description {
279
+ font-size: 0.8rem;
280
+ color: #718096;
281
+ text-align: center;
282
+ margin-bottom: 15px;
283
+ overflow: hidden;
284
+ text-overflow: ellipsis;
285
+ white-space: nowrap;
286
+ }
287
+ body.dark-mode .product-description {
288
+ color: #a0aec0;
289
+ }
290
+ .product-button {
291
+ display: block;
292
+ width: 100%;
293
+ padding: 8px;
294
+ border: none;
295
+ border-radius: 8px;
296
+ background-color: #3b82f6;
297
+ color: white;
298
+ font-size: 0.8rem;
299
+ font-weight: 500;
300
+ cursor: pointer;
301
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
302
+ margin: 5px 0;
303
+ text-align: center;
304
+ text-decoration: none;
305
+ }
306
+ .product-button:hover {
307
+ background-color: #2563eb;
308
+ box-shadow: 0 4px 15px rgba(37, 99, 235, 0.4);
309
+ transform: translateY(-2px);
310
+ }
311
+ .add-to-cart {
312
+ background-color: #10b981;
313
+ }
314
+ .add-to-cart:hover {
315
+ background-color: #059669;
316
+ box-shadow: 0 4px 15px rgba(5, 150, 105, 0.4);
317
+ }
318
+ #cart-button {
319
+ position: fixed;
320
+ bottom: 20px;
321
+ right: 20px;
322
+ background-color: #ef4444;
323
+ color: white;
324
+ border: none;
325
+ border-radius: 50%;
326
+ width: 50px;
327
+ height: 50px;
328
+ font-size: 1.2rem;
329
+ cursor: pointer;
330
+ display: none;
331
+ box-shadow: 0 4px 15px rgba(239, 68, 68, 0.4);
332
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
333
+ z-index: 1000;
334
+ }
335
+ .modal {
336
+ display: none;
337
+ position: fixed;
338
+ z-index: 1001;
339
+ left: 0;
340
+ top: 0;
341
+ width: 100%;
342
+ height: 100%;
343
+ background-color: rgba(0,0,0,0.5);
344
+ backdrop-filter: blur(5px);
345
+ }
346
+ .modal-content {
347
+ background: #fff;
348
+ margin: 5% auto;
349
+ padding: 20px;
350
+ border-radius: 15px;
351
+ width: 90%;
352
+ max-width: 700px;
353
+ box-shadow: 0 10px 30px rgba(0,0,0,0.2);
354
+ animation: slideIn 0.3s ease-out;
355
+ position: relative;
356
+ }
357
+ body.dark-mode .modal-content {
358
+ background: #2d3748;
359
+ color: #e2e8f0;
360
+ }
361
+ @keyframes slideIn {
362
+ from { transform: translateY(-50px); opacity: 0; }
363
+ to { transform: translateY(0); opacity: 1; }
364
+ }
365
+ .close {
366
+ position: absolute;
367
+ top: 10px;
368
+ right: 15px;
369
+ font-size: 1.5rem;
370
+ color: #718096;
371
+ cursor: pointer;
372
+ transition: color 0.3s;
373
+ }
374
+ .close:hover {
375
+ color: #2d3748;
376
+ }
377
+ body.dark-mode .close {
378
+ color: #a0aec0;
379
+ }
380
+ body.dark-mode .close:hover {
381
+ color: #fff;
382
+ }
383
+ .cart-item {
384
+ display: flex;
385
+ justify-content: space-between;
386
+ align-items: center;
387
+ padding: 15px 0;
388
+ border-bottom: 1px solid #e2e8f0;
389
+ }
390
+ body.dark-mode .cart-item {
391
+ border-bottom: 1px solid #4a5568;
392
+ }
393
+ .cart-item img {
394
+ width: 50px;
395
+ height: 50px;
396
+ object-fit: contain;
397
+ border-radius: 8px;
398
+ margin-right: 15px;
399
+ }
400
+ .quantity-input, .color-select {
401
+ width: 100%;
402
+ max-width: 150px;
403
+ padding: 8px;
404
+ border: 1px solid #e2e8f0;
405
+ border-radius: 8px;
406
+ font-size: 1rem;
407
+ margin: 5px 0;
408
+ }
409
+ .clear-cart {
410
+ background-color: #ef4444;
411
+ }
412
+ .clear-cart:hover {
413
+ background-color: #dc2626;
414
+ box-shadow: 0 4px 15px rgba(220, 38, 38, 0.4);
415
+ }
416
+ .order-button {
417
+ background-color: #10b981;
418
+ }
419
+ .order-button:hover {
420
+ background-color: #059669;
421
+ box-shadow: 0 4px 15px rgba(5, 150, 105, 0.4);
422
+ }
423
+ .logo-modal {
424
+ display: none;
425
+ position: fixed;
426
+ top: 0;
427
+ left: 0;
428
+ width: 100%;
429
+ height: 100%;
430
+ background-color: rgba(0, 0, 0, 0.7);
431
+ z-index: 1002;
432
+ justify-content: center;
433
+ align-items: center;
434
+ }
435
+ .logo-modal-content {
436
+ max-width: 90%;
437
+ max-height: 90%;
438
+ position: relative;
439
+ }
440
+ .logo-modal-content img {
441
+ display: block;
442
+ max-width: 100%;
443
+ max-height: 100%;
444
+ margin: auto;
445
+ border-radius: 10px;
446
+ }
447
+ .logo-close {
448
+ position: absolute;
449
+ top: 15px;
450
+ right: 15px;
451
+ font-size: 2rem;
452
+ color: white;
453
+ cursor: pointer;
454
+ z-index: 1003;
455
+ }
456
+ .logo-close:hover {
457
+ color: #ddd;
458
+ }
459
+ </style>
460
+ </head>
461
+ <body>
462
+ <div class="container">
463
+ <div class="header">
464
+ <div class="header-logo-container" onclick="openLogoModal()">
465
+ <img src="''' + LOGO_URL + '''" alt="Logo" class="header-logo">
466
+ </div>
467
+ <h1>Каталог</h1>
468
+ <button class="theme-toggle" onclick="toggleTheme()">
469
+ <i class="fas fa-moon"></i>
470
+ </button>
471
+ </div>
472
+ <div class="filters-container">
473
+ <button class="category-filter active" data-category="all">Все категории</button>
474
+ {% for category in categories %}
475
+ <button class="category-filter" data-category="{{ category }}">{{ category }}</button>
476
+ {% endfor %}
477
+ </div>
478
+ <div class="search-container">
479
+ <input type="text" id="search-input" placeholder="Поиск товаров...">
480
+ </div>
481
+ <div class="products-grid" id="products-grid">
482
+ {% for product in products %}
483
+ <div class="product"
484
+ data-name="{{ product['name']|lower }}"
485
+ data-description="{{ product['description']|lower }}"
486
+ data-category="{{ product.get('category', 'Без категории') }}">
487
+ <a href="{{ url_for('product_detail', index=loop.index0) }}" style="text-decoration: none; color: inherit;">
488
+ {% if product.get('photos') and product['photos']|length > 0 %}
489
+ <div class="product-image">
490
+ <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product['photos'][0] }}"
491
+ alt="{{ product['name'] }}"
492
+ loading="lazy">
493
+ </div>
494
+ {% endif %}
495
+ <h2>{{ product['name'] }}</h2>
496
+ <div class="product-price">${{ product['price'] }}</div>
497
+ <p class="product-description">{{ product['description'][:50] }}{% if product['description']|length > 50 %}...{% endif %}</p>
498
+ </a>
499
+ <button class="product-button add-to-cart" onclick="openQuantityModal({{ loop.index0 }})">В корзину</button>
500
+ </div>
501
+ {% endfor %}
502
+ </div>
503
+ </div>
504
+
505
+ <!-- Quantity and Color Modal -->
506
+ <div id="quantityModal" class="modal">
507
+ <div class="modal-content">
508
+ <span class="close" onclick="closeModal('quantityModal')">×</span>
509
+ <h2>Укажите количество и цвет</h2>
510
+ <input type="number" id="quantityInput" class="quantity-input" min="1" value="1">
511
+ <select id="colorSelect" class="color-select"></select>
512
+ <button class="product-button" onclick="confirmAddToCart()">Добавить</button>
513
+ </div>
514
+ </div>
515
+
516
+ <!-- Cart Modal -->
517
+ <div id="cartModal" class="modal">
518
+ <div class="modal-content">
519
+ <span class="close" onclick="closeModal('cartModal')">×</span>
520
+ <h2>Корзина</h2>
521
+ <div id="cartContent"></div>
522
+ <div style="margin-top: 20px; text-align: right;">
523
+ <strong>Итого: $<span id="cartTotal">0</span></strong>
524
+ <button class="product-button clear-cart" onclick="clearCart()">Очистить</button>
525
+ <button class="product-button order-button" onclick="orderViaWhatsApp()">Заказать</button>
526
+ </div>
527
+ </div>
528
+ </div>
529
+
530
+ <!-- Logo Modal -->
531
+ <div id="logoModal" class="logo-modal">
532
+ <div class="logo-modal-content">
533
+ <span class="logo-close" onclick="closeLogoModal()">×</span>
534
+ <img src="''' + LOGO_URL + '''" alt="Logo">
535
+ </div>
536
+ </div>
537
+
538
+ <button id="cart-button" onclick="openCartModal()">🛒</button>
539
+
540
+ <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
541
+ <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.5.3/dist/umd/popper.min.js"></script>
542
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.js"></script>
543
+ <script>
544
+ const products = {{ products|tojson }};
545
+ let selectedProductIndex = null;
546
+
547
+ function toggleTheme() {
548
+ document.body.classList.toggle('dark-mode');
549
+ const icon = document.querySelector('.theme-toggle i');
550
+ icon.classList.toggle('fa-moon');
551
+ icon.classList.toggle('fa-sun');
552
+ localStorage.setItem('theme', document.body.classList.contains('dark-mode') ? 'dark' : 'light');
553
+ }
554
+
555
+ if (localStorage.getItem('theme') === 'dark') {
556
+ document.body.classList.add('dark-mode');
557
+ document.querySelector('.theme-toggle i').classList.replace('fa-moon', 'fa-sun');
558
+ }
559
+
560
+ function closeModal(modalId) {
561
+ document.getElementById(modalId).style.display = "none";
562
+ }
563
+
564
+ function openLogoModal() {
565
+ document.getElementById('logoModal').style.display = 'flex';
566
+ }
567
+
568
+ function closeLogoModal() {
569
+ document.getElementById('logoModal').style.display = 'none';
570
+ }
571
+
572
+ function openQuantityModal(index) {
573
+ event.stopPropagation();
574
+ selectedProductIndex = index;
575
+ const product = products[index];
576
+ const colorSelect = document.getElementById('colorSelect');
577
+ colorSelect.innerHTML = '';
578
+ if (product.colors && product.colors.length > 0) {
579
+ product.colors.forEach(color => {
580
+ const option = document.createElement('option');
581
+ option.value = color;
582
+ option.text = color;
583
+ colorSelect.appendChild(option);
584
+ });
585
+ } else {
586
+ const option = document.createElement('option');
587
+ option.value = 'Нет цвета';
588
+ option.text = 'Нет цвета';
589
+ colorSelect.appendChild(option);
590
+ }
591
+ document.getElementById('quantityModal').style.display = 'block';
592
+ document.getElementById('quantityInput').value = 1;
593
+ }
594
+
595
+ function confirmAddToCart() {
596
+ if (selectedProductIndex === null) return;
597
+ const quantity = parseInt(document.getElementById('quantityInput').value) || 1;
598
+ const color = document.getElementById('colorSelect').value;
599
+ const product = products[selectedProductIndex];
600
+
601
+ if (quantity < product.roll_meters) {
602
+ alert(`Минимальный заказ для этого товара: ${product.roll_meters} метров.`);
603
+ return;
604
+ }
605
+
606
+ if (quantity <= 0) {
607
+ alert("Укажите количество больше 0");
608
+ return;
609
+ }
610
+ let cart = JSON.parse(localStorage.getItem('cart') || '[]');
611
+
612
+ const cartItemId = `${product.name}-${color}`;
613
+ const existingItem = cart.find(item => item.id === cartItemId);
614
+
615
+ if (existingItem) {
616
+ existingItem.quantity += quantity;
617
+ } else {
618
+ cart.push({
619
+ id: cartItemId,
620
+ name: product.name,
621
+ price: product.price,
622
+ photo: product.photos && product.photos.length > 0 ? product.photos[0] : '',
623
+ quantity: quantity,
624
+ color: color,
625
+ roll_meters: product.roll_meters
626
+ });
627
+ }
628
+
629
+ localStorage.setItem('cart', JSON.stringify(cart));
630
+ closeModal('quantityModal');
631
+ updateCartButton();
632
+ }
633
+
634
+ function updateCartButton() {
635
+ const cart = JSON.parse(localStorage.getItem('cart') || '[]');
636
+ document.getElementById('cart-button').style.display = cart.length > 0 ? 'block' : 'none';
637
+ }
638
+
639
+ function openCartModal() {
640
+ const cart = JSON.parse(localStorage.getItem('cart') || '[]');
641
+ const cartContent = document.getElementById('cartContent');
642
+ let total = 0;
643
+
644
+ cartContent.innerHTML = cart.length === 0 ? '<p>Корзина пуста</p>' : cart.map(item => {
645
+ const itemTotal = item.price * item.quantity;
646
+ total += itemTotal;
647
+ return `
648
+ <div class="cart-item">
649
+ <div style="display: flex; align-items: center;">
650
+ ${item.photo ? `<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/${item.photo}" alt="${item.name}">` : ''}
651
+ <div>
652
+ <strong>${item.name}</strong>
653
+ <p>$${item.price} × ${item.quantity} (Цвет: ${item.color})</p>
654
+ <p>Метров в рулоне: ${item.roll_meters}</p>
655
+ </div>
656
+ </div>
657
+ <span>$${itemTotal}</span>
658
+ </div>
659
+ `;
660
+ }).join('');
661
+
662
+ document.getElementById('cartTotal').textContent = total;
663
+ document.getElementById('cartModal').style.display = 'block';
664
+ }
665
+
666
+ function orderViaWhatsApp() {
667
+ const cart = JSON.parse(localStorage.getItem('cart') || '[]');
668
+ if (cart.length === 0) {
669
+ alert("Корзина пуста!");
670
+ return;
671
+ }
672
+ let total = 0;
673
+ let orderText = "Заказ:%0A";
674
+ cart.forEach((item, index) => {
675
+ const itemTotal = item.price * item.quantity;
676
+ total += itemTotal;
677
+ orderText += `${index + 1}. ${item.name} - $${item.price} × ${item.quantity} (Цвет: ${item.color}, Метров в рулоне: ${item.roll_meters})%0A`;
678
+ });
679
+ orderText += `Итого: $${total}`;
680
+ window.open(`https://api.whatsapp.com/send?phone=996775581999&text=${orderText}`, '_blank');
681
+ }
682
+
683
+ function clearCart() {
684
+ localStorage.removeItem('cart');
685
+ closeModal('cartModal');
686
+ updateCartButton();
687
+ }
688
+
689
+ window.onclick = function(event) {
690
+ if (event.target.className === 'modal') event.target.style.display = "none";
691
+ if (event.target.id === 'logoModal') closeLogoModal();
692
+ }
693
+
694
+ document.getElementById('search-input').addEventListener('input', filterProducts);
695
+ document.querySelectorAll('.category-filter').forEach(filter => {
696
+ filter.addEventListener('click', function() {
697
+ document.querySelectorAll('.category-filter').forEach(f => f.classList.remove('active'));
698
+ this.classList.add('active');
699
+ filterProducts();
700
+ });
701
+ });
702
+
703
+ function filterProducts() {
704
+ const searchTerm = document.getElementById('search-input').value.toLowerCase();
705
+ const activeCategory = document.querySelector('.category-filter.active').dataset.category;
706
+ document.querySelectorAll('.product').forEach(product => {
707
+ const name = product.getAttribute('data-name');
708
+ const description = product.getAttribute('data-description');
709
+ const category = product.getAttribute('data-category');
710
+ const matchesSearch = name.includes(searchTerm) || description.includes(searchTerm);
711
+ const matchesCategory = activeCategory === 'all' || category === activeCategory;
712
+ product.style.display = matchesSearch && matchesCategory ? 'block' : 'none';
713
+ });
714
+ }
715
+
716
+ updateCartButton();
717
+ </script>
718
+ </body>
719
+ </html>
720
+ '''
721
+ return render_template_string(catalog_html, products=products, categories=categories, repo_id=REPO_ID)
722
+
723
+ @app.route('/product/<int:index>')
724
+ def product_detail(index):
725
+ data = load_data()
726
+ products = data['products']
727
+ try:
728
+ product = products[index]
729
+ except IndexError:
730
+ return "Продукт не найден", 404
731
+
732
+ detail_html = '''
733
+ <!DOCTYPE html>
734
+ <html lang="ru">
735
+ <head>
736
+ <meta charset="UTF-8">
737
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
738
+ <title>{{ product['name'] }} - У Деда</title>
739
+ <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
740
+ <style>
741
+ * {
742
+ margin: 0;
743
+ padding: 0;
744
+ box-sizing: border-box;
745
+ }
746
+ body {
747
+ font-family: 'Poppins', sans-serif;
748
+ background: linear-gradient(135deg, #f0f2f5, #e9ecef);
749
+ color: #2d3748;
750
+ line-height: 1.6;
751
+ }
752
+ .container {
753
+ max-width: 1300px;
754
+ margin: 0 auto;
755
+ padding: 20px;
756
+ }
757
+ .header {
758
+ display: flex;
759
+ justify-content: space-between;
760
+ align-items: center;
761
+ padding: 15px 0;
762
+ border-bottom: 1px solid #e2e8f0;
763
+ }
764
+ .header-logo {
765
+ width: 60px;
766
+ height: 60px;
767
+ border-radius: 50%;
768
+ object-fit: cover;
769
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
770
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
771
+ }
772
+ .header-logo:hover {
773
+ transform: scale(1.1);
774
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
775
+ }
776
+ .header h1 {
777
+ font-size: 1.5rem;
778
+ font-weight: 600;
779
+ margin-left: 15px;
780
+ }
781
+ .product-detail-container {
782
+ padding: 20px;
783
+ }
784
+ .product-detail-container h2 {
785
+ font-size: 1.8rem;
786
+ font-weight: 600;
787
+ margin-bottom: 20px;
788
+ text-align: center;
789
+ }
790
+ .image-grid {
791
+ display: grid;
792
+ grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
793
+ gap: 10px;
794
+ margin-bottom: 20px;
795
+ }
796
+ .image-grid img {
797
+ width: 100%;
798
+ height: 100px;
799
+ object-fit: cover;
800
+ border-radius: 8px;
801
+ cursor: pointer;
802
+ transition: transform 0.3s ease;
803
+ }
804
+ .image-grid img:hover {
805
+ transform: scale(1.1);
806
+ }
807
+ .photo-modal {
808
+ display: none;
809
+ position: fixed;
810
+ top: 0;
811
+ left: 0;
812
+ width: 100%;
813
+ height: 100%;
814
+ background-color: rgba(0, 0, 0, 0.7);
815
+ z-index: 1003;
816
+ justify-content: center;
817
+ align-items: center;
818
+ }
819
+ .photo-modal-content {
820
+ width: 100%;
821
+ height: 100%;
822
+ position: relative;
823
+ display: flex;
824
+ justify-content: center;
825
+ align-items: center;
826
+ padding: 20px;
827
+ }
828
+ .photo-modal-content img {
829
+ width: 100%;
830
+ height: 100%;
831
+ object-fit: contain;
832
+ border-radius: 10px;
833
+ }
834
+ .photo-close {
835
+ position: absolute;
836
+ top: 15px;
837
+ right: 15px;
838
+ font-size: 2rem;
839
+ color: white;
840
+ cursor: pointer;
841
+ z-index: 1004;
842
+ }
843
+ .photo-close:hover {
844
+ color: #ddd;
845
+ }
846
+ .product-info p {
847
+ margin: 10px 0;
848
+ font-size: 1rem;
849
+ }
850
+ .back-button {
851
+ display: block;
852
+ width: 100%;
853
+ max-width: 300px;
854
+ margin: 20px auto;
855
+ padding: 12px;
856
+ border: none;
857
+ border-radius: 8px;
858
+ background-color: #3b82f6;
859
+ color: white;
860
+ font-size: 1rem;
861
+ font-weight: 500;
862
+ cursor: pointer;
863
+ text-align: center;
864
+ text-decoration: none;
865
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
866
+ }
867
+ .back-button:hover {
868
+ background-color: #2563eb;
869
+ box-shadow: 0 4px 15px rgba(37, 99, 235, 0.4);
870
+ transform: translateY(-2px);
871
+ }
872
+ /* Оптимизация для мобильных устройств */
873
+ @media (max-width: 768px) {
874
+ .photo-modal-content {
875
+ padding: 10px;
876
+ }
877
+ .photo-modal-content img {
878
+ max-width: 90%;
879
+ max-height: 90%;
880
+ width: auto;
881
+ height: auto;
882
+ }
883
+ .photo-close {
884
+ font-size: 1.5rem;
885
+ top: 10px;
886
+ right: 10px;
887
+ }
888
+ .product-detail-container h2 {
889
+ font-size: 1.5rem;
890
+ }
891
+ .header h1 {
892
+ font-size: 1.2rem;
893
+ }
894
+ .image-grid img {
895
+ height: 80px;
896
+ }
897
+ .product-info p {
898
+ font-size: 0.9rem;
899
+ }
900
+ }
901
+ </style>
902
+ </head>
903
+ <body>
904
+ <div class="container">
905
+ <div class="header">
906
+ <a href="/">
907
+ <img src="''' + LOGO_URL + '''" alt="Logo" class="header-logo">
908
+ </a>
909
+ <h1>{{ product['name'] }}</h1>
910
+ </div>
911
+
912
+ <div class="product-detail-container">
913
+ <h2>{{ product['name'] }}</h2>
914
+
915
+ <div class="image-grid">
916
+ {% if product.get('photos') %}
917
+ {% for photo in product['photos'] %}
918
+ <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo }}"
919
+ alt="{{ product['name'] }}"
920
+ onclick="openPhotoModal('https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo }}')">
921
+ {% endfor %}
922
+ {% else %}
923
+ <p>Нет фотографий</p>
924
+ {% endif %}
925
+ </div>
926
+
927
+ <div id="photoModal" class="photo-modal">
928
+ <div class="photo-modal-content">
929
+ <span class="photo-close" onclick="closePhotoModal()">×</span>
930
+ <img id="photoImage" src="" alt="Фото товара">
931
+ </div>
932
+ </div>
933
+
934
+ <div class="product-info">
935
+ <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
936
+ <p><strong>Цена:</strong> ${{ product['price'] }}</p>
937
+ <p><strong>Состав:</strong> {{ product['description'] }}</p>
938
+ <p><strong>Доступные цвета:</strong> {{ product.get('colors', ['Нет цветов'])|join(', ') }}</p>
939
+ <p><strong>Метров в рулоне:</strong> {{ product.get('roll_meters', 'Не указано') }}</p>
940
+ </div>
941
+
942
+ <a href="/" class="back-button">Вернуться в каталог</a>
943
+ </div>
944
+ </div>
945
+
946
+ <script>
947
+ function openPhotoModal(src) {
948
+ const modal = document.getElementById('photoModal');
949
+ document.getElementById('photoImage').src = src;
950
+ modal.style.display = 'flex';
951
+ }
952
+
953
+ function closePhotoModal() {
954
+ document.getElementById('photoModal').style.display = 'none';
955
+ }
956
+
957
+ window.onclick = function(event) {
958
+ const modal = document.getElementById('photoModal');
959
+ if (event.target === modal) {
960
+ closePhotoModal();
961
+ }
962
+ }
963
+
964
+ document.addEventListener('keydown', function(event) {
965
+ if (event.key === 'Escape') {
966
+ closePhotoModal();
967
+ }
968
+ });
969
+ </script>
970
+ </body>
971
+ </html>
972
+ '''
973
+ return render_template_string(detail_html, product=product, repo_id=REPO_ID)
974
+
975
+ @app.route('/admin', methods=['GET', 'POST'])
976
+ def admin():
977
+ data = load_data()
978
+ products = data['products']
979
+ categories = data['categories']
980
+
981
+ if request.method == 'POST':
982
+ action = request.form.get('action')
983
+
984
+ if action == 'add_category':
985
+ category_name = request.form.get('category_name')
986
+ if category_name and category_name not in categories:
987
+ categories.append(category_name)
988
+ save_data(data)
989
+ return redirect(url_for('admin'))
990
+ return "Ошибка: Категория уже существует или не указано название", 400
991
+
992
+ elif action == 'delete_category':
993
+ category_index = int(request.form.get('category_index'))
994
+ deleted_category = categories.pop(category_index)
995
+ for product in products:
996
+ if product.get('category') == deleted_category:
997
+ product['category'] = 'Без категории'
998
+ save_data(data)
999
+ return redirect(url_for('admin'))
1000
+
1001
+ elif action == 'add':
1002
+ name = request.form.get('name')
1003
+ price = request.form.get('price')
1004
+ description = request.form.get('description')
1005
+ category = request.form.get('category')
1006
+ photos_files = request.files.getlist('photos')
1007
+ colors = request.form.getlist('colors')
1008
+ roll_meters = request.form.get('roll_meters')
1009
+ photos_list = []
1010
+
1011
+ if photos_files:
1012
+ for photo in photos_files[:10]:
1013
+ if photo and photo.filename:
1014
+ photo_filename = secure_filename(photo.filename)
1015
+ uploads_dir = 'uploads'
1016
+ os.makedirs(uploads_dir, exist_ok=True)
1017
+
1018
+ temp_path = os.path.join(uploads_dir, photo_filename)
1019
+ photo.save(temp_path)
1020
+ api = HfApi()
1021
+ api.upload_file(
1022
+ path_or_fileobj=temp_path,
1023
+ path_in_repo=f"photos/{photo_filename}",
1024
+ repo_id=REPO_ID,
1025
+ repo_type="dataset",
1026
+ token=HF_TOKEN_WRITE,
1027
+ commit_message=f"Добавлено фото для товара {name}"
1028
+ )
1029
+ photos_list.append(photo_filename)
1030
+ if os.path.exists(temp_path):
1031
+ os.remove(temp_path)
1032
+
1033
+ if not name or not price or not description or not roll_meters:
1034
+ return "Ошибка: Заполните все обязательные поля", 400
1035
+
1036
+ price = float(price.replace(',', '.'))
1037
+ try:
1038
+ roll_meters = int(roll_meters)
1039
+ except ValueError:
1040
+ return "Ошибка: Количество метров в рулоне должно быть целым числом", 400
1041
+
1042
+ new_product = {
1043
+ 'name': name,
1044
+ 'price': price,
1045
+ 'description': description,
1046
+ 'category': category if category in categories else 'Без категории',
1047
+ 'photos': photos_list,
1048
+ 'colors': colors if colors else [],
1049
+ 'roll_meters': roll_meters
1050
+ }
1051
+ products.append(new_product)
1052
+ save_data(data)
1053
+ return redirect(url_for('admin'))
1054
+
1055
+ elif action == 'edit':
1056
+ index = int(request.form.get('index'))
1057
+ name = request.form.get('name')
1058
+ price = request.form.get('price')
1059
+ description = request.form.get('description')
1060
+ category = request.form.get('category')
1061
+ photos_files = request.files.getlist('photos')
1062
+ colors = request.form.getlist('colors')
1063
+ roll_meters = request.form.get('roll_meters')
1064
+
1065
+ if photos_files and any(photo.filename for photo in photos_files):
1066
+ new_photos_list = []
1067
+ for photo in photos_files[:10]:
1068
+ if photo and photo.filename:
1069
+ photo_filename = secure_filename(photo.filename)
1070
+ uploads_dir = 'uploads'
1071
+ os.makedirs(uploads_dir, exist_ok=True)
1072
+ temp_path = os.path.join(uploads_dir, photo_filename)
1073
+ photo.save(temp_path)
1074
+ api = HfApi()
1075
+ api.upload_file(
1076
+ path_or_fileobj=temp_path,
1077
+ path_in_repo=f"photos/{photo_filename}",
1078
+ repo_id=REPO_ID,
1079
+ repo_type="dataset",
1080
+ token=HF_TOKEN_WRITE,
1081
+ commit_message=f"Обновлено фото для товара {name}"
1082
+ )
1083
+ new_photos_list.append(photo_filename)
1084
+ if os.path.exists(temp_path):
1085
+ os.remove(temp_path)
1086
+ products[index]['photos'] = new_photos_list
1087
+
1088
+ if not roll_meters:
1089
+ return "Ошибка: Укажите количество метров в рулоне", 400
1090
+
1091
+ try:
1092
+ roll_meters = int(roll_meters)
1093
+ except ValueError:
1094
+ return "Ошибка: Количество метров в рулоне должно быть целым числом", 400
1095
+
1096
+ products[index]['name'] = name
1097
+ products[index]['price'] = float(price.replace(',', '.'))
1098
+ products[index]['description'] = description
1099
+ products[index]['category'] = category if category in categories else 'Без категории'
1100
+ products[index]['colors'] = colors if colors else []
1101
+ products[index]['roll_meters'] = roll_meters
1102
+ save_data(data)
1103
+ return redirect(url_for('admin'))
1104
+
1105
+ elif action == 'delete':
1106
+ index = int(request.form.get('index'))
1107
+ del products[index]
1108
+ save_data(data)
1109
+ return redirect(url_for('admin'))
1110
+
1111
+ elif action == 'upload_db':
1112
+ file = request.files.get('db_file')
1113
+ if file and file.filename.endswith('.json'):
1114
+ try:
1115
+ file_data = json.load(file)
1116
+ if 'products' in file_data and 'categories' in file_data:
1117
+ save_data(file_data)
1118
+ return redirect(url_for('admin'))
1119
+ else:
1120
+ return "Ошибка: Неверная структура JSON файла.", 400
1121
+ except json.JSONDecodeError:
1122
+ return "Ошибка: Невозможно декодировать JSON файл.", 400
1123
+ else:
1124
+ return "Ошибка: Выберите JSON файл.", 400
1125
+
1126
+ admin_html = '''
1127
+ <!DOCTYPE html>
1128
+ <html lang="ru">
1129
+ <head>
1130
+ <meta charset="UTF-8">
1131
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1132
+ <title>Админ-панель</title>
1133
+ <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
1134
+ <style>
1135
+ body {
1136
+ font-family: 'Poppins', sans-serif;
1137
+ background: linear-gradient(135deg, #f0f2f5, #e9ecef);
1138
+ color: #2d3748;
1139
+ padding: 20px;
1140
+ }
1141
+ .container {
1142
+ max-width: 1200px;
1143
+ margin: 0 auto;
1144
+ }
1145
+ .header {
1146
+ display: flex;
1147
+ align-items: center;
1148
+ padding: 15px 0;
1149
+ border-bottom: 1px solid #e2e8f0;
1150
+ }
1151
+ .header-logo {
1152
+ width: 60px;
1153
+ height: 60px;
1154
+ border-radius: 50%;
1155
+ object-fit: cover;
1156
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
1157
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
1158
+ margin-right: 15px;
1159
+ }
1160
+ .header-logo:hover {
1161
+ transform: scale(1.1);
1162
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
1163
+ }
1164
+ h1, h2 {
1165
+ font-weight: 600;
1166
+ margin-bottom: 20px;
1167
+ }
1168
+ form {
1169
+ background: #fff;
1170
+ padding: 20px;
1171
+ border-radius: 15px;
1172
+ box-shadow: 0 4px 15px rgba(0,0,0,0.1);
1173
+ margin-bottom: 30px;
1174
+ }
1175
+ label {
1176
+ font-weight: 500;
1177
+ margin-top: 15px;
1178
+ display: block;
1179
+ }
1180
+ input, textarea, select {
1181
+ width: 100%;
1182
+ padding: 12px;
1183
+ margin-top: 5px;
1184
+ border: 1px solid #e2e8f0;
1185
+ border-radius: 8px;
1186
+ font-size: 1rem;
1187
+ transition: all 0.3s ease;
1188
+ }
1189
+ input:focus, textarea:focus, select:focus {
1190
+ border-color: #3b82f6;
1191
+ box-shadow: 0 0 5px rgba(59, 130, 246, 0.3);
1192
+ outline: none;
1193
+ }
1194
+ button {
1195
+ padding: 12px 20px;
1196
+ border: none;
1197
+ border-radius: 8px;
1198
+ background-color: #3b82f6;
1199
+ color: white;
1200
+ font-weight: 500;
1201
+ cursor: pointer;
1202
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
1203
+ margin-top: 15px;
1204
+ }
1205
+ button:hover {
1206
+ background-color: #2563eb;
1207
+ box-shadow: 0 4px 15px rgba(37, 99, 235, 0.4);
1208
+ transform: translateY(-2px);
1209
+ }
1210
+ .delete-button {
1211
+ background-color: #ef4444;
1212
+ }
1213
+ .delete-button:hover {
1214
+ background-color: #dc2626;
1215
+ box-shadow: 0 4px 15px rgba(220, 38, 38, 0.4);
1216
+ }
1217
+ .product-list, .category-list {
1218
+ display: grid;
1219
+ gap: 20px;
1220
+ }
1221
+ .product-item, .category-item {
1222
+ background: #fff;
1223
+ padding: 20px;
1224
+ border-radius: 15px;
1225
+ box-shadow: 0 4px 15px rgba(0,0,0,0.1);
1226
+ }
1227
+ .edit-form {
1228
+ margin-top: 15px;
1229
+ padding: 15px;
1230
+ background: #f7fafc;
1231
+ border-radius: 10px;
1232
+ }
1233
+ .color-input-group {
1234
+ display: flex;
1235
+ gap: 10px;
1236
+ margin-top: 5px;
1237
+ }
1238
+ .add-color-btn {
1239
+ background-color: #10b981;
1240
+ }
1241
+ .add-color-btn:hover {
1242
+ background-color: #059669;
1243
+ }
1244
+ </style>
1245
+ </head>
1246
+ <body>
1247
+ <div class="container">
1248
+ <div class="header">
1249
+ <img src="''' + LOGO_URL + '''" alt="Logo" class="header-logo">
1250
+ <h1>Админ-панель</h1>
1251
+ </div>
1252
+ <h1>Добавление товара</h1>
1253
+ <form method="POST" enctype="multipart/form-data">
1254
+ <input type="hidden" name="action" value="add">
1255
+ <label>Название товара:</label>
1256
+ <input type="text" name="name" required>
1257
+ <label>Цена ($):</label>
1258
+ <input type="number" name="price" step="0.01" required>
1259
+ <label>Состав:</label>
1260
+ <textarea name="description" rows="4" required></textarea>
1261
+ <label>Категория:</label>
1262
+ <select name="category">
1263
+ <option value="Без категории">Без категории</option>
1264
+ {% for category in categories %}
1265
+ <option value="{{ category }}">{{ category }}</option>
1266
+ {% endfor %}
1267
+ </select>
1268
+ <label>Фотографии (до 10):</label>
1269
+ <input type="file" name="photos" accept="image/*" multiple>
1270
+ <label>Цвета:</label>
1271
+ <div id="color-inputs">
1272
+ <div class="color-input-group">
1273
+ <input type="text" name="colors" placeholder="Например: Красный">
1274
+ </div>
1275
+ </div>
1276
+ <button type="button" class="add-color-btn" onclick="addColorInput()">Добавить цвет</button>
1277
+ <label>Метров в рулоне:</label>
1278
+ <input type="number" name="roll_meters" required>
1279
+ <button type="submit">Добавить товар</button>
1280
+ </form>
1281
+
1282
+ <h1>Управление категориями</h1>
1283
+ <form method="POST">
1284
+ <input type="hidden" name="action" value="add_category">
1285
+ <label>Название категории:</label>
1286
+ <input type="text" name="category_name" required>
1287
+ <button type="submit">Добавить</button>
1288
+ </form>
1289
+
1290
+ <h2>Список категорий</h2>
1291
+ <div class="category-list">
1292
+ {% for category in categories %}
1293
+ <div class="category-item">
1294
+ <h3>{{ category }}</h3>
1295
+ <form method="POST" style="display: inline;">
1296
+ <input type="hidden" name="action" value="delete_category">
1297
+ <input type="hidden" name="category_index" value="{{ loop.index0 }}">
1298
+ <button type="submit" class="delete-button">Удалить</button>
1299
+ </form>
1300
+ </div>
1301
+ {% endfor %}
1302
+ </div>
1303
+
1304
+ <h2>Управление базой данных</h2>
1305
+ <form method="POST" action="{{ url_for('backup') }}" style="display: inline;">
1306
+ <button type="submit">Создать копию</button>
1307
+ </form>
1308
+ <form method="GET" action="{{ url_for('download') }}" style="display: inline;">
1309
+ <button type="submit">Скачать базу</button>
1310
+ </form>
1311
+
1312
+ <form method="POST" enctype="multipart/form-data">
1313
+ <input type="hidden" name="action" value="upload_db">
1314
+ <label>Загрузить JSON файл базы данных:</label>
1315
+ <input type="file" name="db_file" accept=".json" required>
1316
+ <button type="submit">Загрузить</button>
1317
+ </form>
1318
+
1319
+ <h2>Список товаров</h2>
1320
+ <div class="product-list">
1321
+ {% for product in products %}
1322
+ <div class="product-item">
1323
+ <h3>{{ product['name'] }}</h3>
1324
+ <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
1325
+ <p><strong>Цена:</strong> ${{ product['price'] }}</p>
1326
+ <p><strong>Состав:</strong> {{ product['description'] }}</p>
1327
+ <p><strong>Цвета:</strong> {{ product.get('colors', ['Нет цветов'])|join(', ') }}</p>
1328
+ <p><strong>Метров в рулоне:</strong> {{ product.get('roll_meters', 'Не указано') }}</p>
1329
+ {% if product.get('photos') and product['photos']|length > 0 %}
1330
+ <div style="display: flex; flex-wrap: wrap; gap: 10px;">
1331
+ {% for photo in product['photos'] %}
1332
+ <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo }}"
1333
+ alt="{{ product['name'] }}"
1334
+ style="max-width: 100px; border-radius: 10px;">
1335
+ {% endfor %}
1336
+ </div>
1337
+ {% endif %}
1338
+ <details>
1339
+ <summary>Редактировать</summary>
1340
+ <form method="POST" enctype="multipart/form-data" class="edit-form">
1341
+ <input type="hidden" name="action" value="edit">
1342
+ <input type="hidden" name="index" value="{{ loop.index0 }}">
1343
+ <label>Название:</label>
1344
+ <input type="text" name="name" value="{{ product['name'] }}" required>
1345
+ <label>Цена ($):</label>
1346
+ <input type="number" name="price" step="0.01" value="{{ product['price'] }}" required>
1347
+ <label>Состав:</label>
1348
+ <textarea name="description" rows="4" required>{{ product['description'] }}</textarea>
1349
+ <label>К��тегория:</label>
1350
+ <select name="category">
1351
+ <option value="Без категории" {% if product.get('category', 'Без категории') == 'Без категории' %}selected{% endif %}>Без категории</option>
1352
+ {% for category in categories %}
1353
+ <option value="{{ category }}" {% if product.get('category') == category %}selected{% endif %}>{{ category }}</option>
1354
+ {% endfor %}
1355
+ </select>
1356
+ <label>Фотографии (до 10):</label>
1357
+ <input type="file" name="photos" accept="image/*" multiple>
1358
+ <label>Цвета:</label>
1359
+ <div id="edit-color-inputs-{{ loop.index0 }}">
1360
+ {% for color in product.get('colors', []) %}
1361
+ <div class="color-input-group">
1362
+ <input type="text" name="colors" value="{{ color }}">
1363
+ </div>
1364
+ {% endfor %}
1365
+ </div>
1366
+ <button type="button" class="add-color-btn" onclick="addColorInput('edit-color-inputs-{{ loop.index0 }}')">Добавить цвет</button>
1367
+ <label>Метров в рулоне:</label>
1368
+ <input type="number" name="roll_meters" value="{{ product.get('roll_meters', '') }}" required>
1369
+ <button type="submit">Сохранить</button>
1370
+ </form>
1371
+ </details>
1372
+ <form method="POST">
1373
+ <input type="hidden" name="action" value="delete">
1374
+ <input type="hidden" name="index" value="{{ loop.index0 }}">
1375
+ <button type="submit" class="delete-button">Удалить</button>
1376
+ </form>
1377
+ </div>
1378
+ {% endfor %}
1379
+ </div>
1380
+ </div>
1381
+ <script>
1382
+ function addColorInput(containerId = 'color-inputs') {
1383
+ const container = document.getElementById(containerId);
1384
+ const newInput = document.createElement('div');
1385
+ newInput.className = 'color-input-group';
1386
+ newInput.innerHTML = '<input type="text" name="colors" placeholder="Например: Красный">';
1387
+ container.appendChild(newInput);
1388
+ }
1389
+ </script>
1390
+ </body>
1391
+ </html>
1392
+ '''
1393
+ return render_template_string(admin_html, products=products, categories=categories, repo_id=REPO_ID)
1394
+
1395
+ @app.route('/backup', methods=['POST'])
1396
+ def backup():
1397
+ upload_db_to_hf()
1398
+ return "Резервная копия создана.", 200
1399
+
1400
+ @app.route('/download', methods=['GET'])
1401
+ def download():
1402
+ download_db_from_hf()
1403
+ return "База данных скачана.", 200
1404
+
1405
+ if __name__ == '__main__':
1406
+ backup_thread = threading.Thread(target=periodic_backup, daemon=True)
1407
+ backup_thread.start()
1408
+ try:
1409
+ load_data()
1410
+ except Exception as e:
1411
+ logging.error(f"Не удалось загрузить базу данных: {e}")
1412
+ app.run(debug=True, host='0.0.0.0', port=7860)