Kgshop commited on
Commit
1beb45f
·
verified ·
1 Parent(s): 93cc1dd

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -1840
app.py DELETED
@@ -1,1840 +0,0 @@
1
- from flask import Flask, render_template_string, request, redirect, url_for, send_file, flash
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
- import random
12
- import uuid
13
-
14
- app = Flask(__name__)
15
- app.secret_key = os.getenv("FLASK_SECRET_KEY", "zzirix_secret_key_for_cart_and_flashes")
16
- DATA_FILE = 'data_zzirix.json'
17
- REPO_ID = "Kgshop/clients"
18
- HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
19
- HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") or HF_TOKEN_WRITE
20
-
21
- LOGO_URL = "https://huggingface.co/spaces/Kgshop/Zzirixadm/resolve/main/Picsart_25-03-20_15-38-36-600.jpg"
22
-
23
- logging.basicConfig(level=logging.INFO)
24
-
25
- def initialize_data_structure(data):
26
- if not isinstance(data, dict):
27
- data = {'categories': [], 'products': [], 'orders': {}}
28
-
29
- data.setdefault('categories', [])
30
- data.setdefault('products', [])
31
- data.setdefault('orders', {})
32
-
33
- for product in data['products']:
34
- if 'id' not in product:
35
- product['id'] = str(uuid.uuid4())
36
-
37
- product.setdefault('name', 'Без названия')
38
- product.setdefault('description', '')
39
- product.setdefault('category', 'Без категории')
40
- product.setdefault('price', 0.0)
41
- product.setdefault('colors', [])
42
- product.setdefault('models', [])
43
-
44
- if 'photos' not in product:
45
- if 'media' in product:
46
- product['photos'] = [item['filename'] for item in product['media'] if item['type'] == 'photo']
47
- del product['media']
48
- else:
49
- product['photos'] = []
50
- if not isinstance(product['photos'], list):
51
- product['photos'] = []
52
-
53
- if 'media' in product: # Ensure 'media' field is completely removed if it exists
54
- del product['media']
55
-
56
- product.pop('in_stock', None) # Remove obsolete fields
57
- product.pop('is_top', None)
58
-
59
- return data
60
-
61
- def load_data():
62
- try:
63
- try:
64
- download_db_from_hf()
65
- except RepositoryNotFoundError:
66
- logging.warning("Hugging Face repository not found or inaccessible. Proceeding with local data or empty structure.")
67
- if not os.path.exists(DATA_FILE):
68
- with open(DATA_FILE, 'w', encoding='utf-8') as f:
69
- json.dump(initialize_data_structure({}), f)
70
- except Exception as e:
71
- logging.error(f"Error during initial HF download: {e}. Proceeding with local data or empty structure.")
72
- if not os.path.exists(DATA_FILE):
73
- with open(DATA_FILE, 'w', encoding='utf-8') as f:
74
- json.dump(initialize_data_structure({}), f)
75
-
76
- if os.path.exists(DATA_FILE) and os.path.getsize(DATA_FILE) > 0:
77
- with open(DATA_FILE, 'r', encoding='utf-8') as file:
78
- data = json.load(file)
79
- logging.info("Данные успешно загружены из JSON.")
80
- else:
81
- data = {'products': [], 'categories': [], 'orders': {}}
82
-
83
- return initialize_data_structure(data)
84
- except (json.JSONDecodeError, FileNotFoundError):
85
- logging.error(f"Ошибка при чтении {DATA_FILE}. Создается пустая структура.")
86
- return initialize_data_structure({})
87
- except Exception as e:
88
- logging.error(f"Ошибка при загрузке данных: {e}")
89
- return initialize_data_structure({})
90
-
91
- def save_data(data):
92
- try:
93
- temp_file = DATA_FILE + '.tmp'
94
- with open(temp_file, 'w', encoding='utf-8') as file:
95
- json.dump(data, file, ensure_ascii=False, indent=4)
96
- os.replace(temp_file, DATA_FILE)
97
- logging.info("Данные успешно сохранены в JSON.")
98
- upload_db_to_hf()
99
- except Exception as e:
100
- logging.error(f"Ошибка при сохранении данных: {e}")
101
- if os.path.exists(temp_file):
102
- os.remove(temp_file)
103
- raise
104
-
105
- def upload_db_to_hf():
106
- if not HF_TOKEN_WRITE:
107
- logging.warning("HF_TOKEN_WRITE не установлен. Пропуск выгрузки.")
108
- return
109
- try:
110
- api = HfApi()
111
- api.upload_file(
112
- path_or_fileobj=DATA_FILE,
113
- path_in_repo=DATA_FILE,
114
- repo_id=REPO_ID,
115
- repo_type="dataset",
116
- token=HF_TOKEN_WRITE,
117
- commit_message=f"Автоматическое резервное копирование базы данных {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
118
- )
119
- logging.info("Резервная копия JSON базы успешно загружена на Hugging Face.")
120
- except Exception as e:
121
- logging.error(f"Ошибка при загрузке резервной копии: {e}")
122
-
123
- def download_db_from_hf():
124
- if not HF_TOKEN_READ:
125
- logging.warning("HF_TOKEN_READ не установлен. Пропуск загрузки.")
126
- if not os.path.exists(DATA_FILE):
127
- with open(DATA_FILE, 'w', encoding='utf-8') as f:
128
- json.dump(initialize_data_structure({}), f)
129
- return
130
- try:
131
- hf_hub_download(
132
- repo_id=REPO_ID,
133
- filename=DATA_FILE,
134
- repo_type="dataset",
135
- token=HF_TOKEN_READ,
136
- local_dir=".",
137
- local_dir_use_symlinks=False,
138
- force_download=True
139
- )
140
- logging.info("JSON база успешно скачана из Hugging Face.")
141
- except RepositoryNotFoundError as e:
142
- logging.error(f"Репозиторий не найден: {e}. Пропускаем скачивание.")
143
- raise
144
- except Exception as e:
145
- logging.error(f"Ошибка при скачивании JSON базы: {e}. Пропускаем скачивание.")
146
- raise
147
-
148
- def periodic_backup():
149
- while True:
150
- try:
151
- upload_db_to_hf()
152
- except Exception as e:
153
- logging.error(f"Ошибка при выполнении периодического резервного копирования: {e}")
154
- time.sleep(3600)
155
-
156
- def allowed_file(filename):
157
- return '.' in filename and \
158
- filename.rsplit('.', 1)[1].lower() in {'png', 'jpg', 'jpeg', 'gif', 'webp'}
159
-
160
- @app.route('/')
161
- def catalog():
162
- data = load_data()
163
- products = data['products']
164
- categories = sorted(data['categories'])
165
-
166
- catalog_html = '''
167
- <!DOCTYPE html>
168
- <html lang="ru">
169
- <head>
170
- <meta charset="UTF-8">
171
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
172
- <title>ZZIRIX - сотовые аксессуары оптом </title>
173
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
174
- <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
175
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.css">
176
- <style>
177
- :root {
178
- --primary-color: #3B82F6;
179
- --primary-dark-color: #2563eb;
180
- --accent-color: #10b981;
181
- --accent-dark-color: #059669;
182
- --danger-color: #ef4444;
183
- --danger-dark-color: #dc2626;
184
- --background-light: linear-gradient(135deg, #f8f9fa, #e9ecef);
185
- --background-dark: linear-gradient(135deg, #1a202c, #2d3748);
186
- --card-background-light: #ffffff;
187
- --card-background-dark: #2d3748;
188
- --text-color-light: #2d3748;
189
- --text-color-dark: #e2e8f0;
190
- --secondary-text-color-light: #718096;
191
- --secondary-text-color-dark: #a0aec0;
192
- --border-color-light: #e2e8f0;
193
- --border-color-dark: #4a5568;
194
- --shadow-light: 0 6px 20px rgba(0, 0, 0, 0.08);
195
- --shadow-hover-light: 0 10px 30px rgba(0, 0, 0, 0.15);
196
- --shadow-dark: 0 6px 20px rgba(0, 0, 0, 0.25);
197
- --shadow-hover-dark: 0 10px 30px rgba(0, 0, 0, 0.4);
198
- }
199
- * {
200
- margin: 0;
201
- padding: 0;
202
- box-sizing: border-box;
203
- }
204
- body {
205
- font-family: 'Roboto', sans-serif;
206
- background: var(--background-light);
207
- color: var(--text-color-light);
208
- line-height: 1.6;
209
- transition: background 0.3s, color 0.3s;
210
- min-height: 100vh;
211
- display: flex;
212
- flex-direction: column;
213
- }
214
- body.dark-mode {
215
- background: var(--background-dark);
216
- color: var(--text-color-dark);
217
- }
218
- .container {
219
- max-width: 1300px;
220
- margin: 0 auto;
221
- padding: 20px;
222
- flex-grow: 1;
223
- }
224
- .header {
225
- display: flex;
226
- justify-content: space-between;
227
- align-items: center;
228
- padding: 15px 0;
229
- border-bottom: 1px solid var(--border-color-light);
230
- margin-bottom: 20px;
231
- }
232
- body.dark-mode .header {
233
- border-bottom-color: var(--border-color-dark);
234
- }
235
- .header-info {
236
- display: flex;
237
- align-items: center;
238
- }
239
- .header-logo {
240
- width: 50px;
241
- height: 50px;
242
- border-radius: 50%;
243
- object-fit: cover;
244
- box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
245
- transition: transform 0.3s ease, box-shadow 0.3s ease;
246
- }
247
- .header-logo:hover {
248
- transform: scale(1.05);
249
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
250
- }
251
- .header h1 {
252
- font-size: 1.7rem;
253
- font-weight: 700;
254
- margin-left: 15px;
255
- }
256
- .theme-toggle {
257
- background: none;
258
- border: none;
259
- font-size: 1.6rem;
260
- cursor: pointer;
261
- color: var(--secondary-text-color-light);
262
- transition: color 0.3s ease, transform 0.2s ease;
263
- padding: 5px;
264
- }
265
- body.dark-mode .theme-toggle {
266
- color: var(--secondary-text-color-dark);
267
- }
268
- .theme-toggle:hover {
269
- color: var(--primary-color);
270
- transform: rotate(15deg);
271
- }
272
-
273
- .flash {
274
- padding: 15px;
275
- margin-bottom: 20px;
276
- border-radius: 8px;
277
- font-weight: 500;
278
- border: 1px solid transparent;
279
- }
280
- .flash.success {
281
- background-color: #d1fae5;
282
- color: #065f46;
283
- border-color: #34d399;
284
- }
285
- .flash.error {
286
- background-color: #fee2e2;
287
- color: #991b1b;
288
- border-color: #ef4444;
289
- }
290
-
291
- .filters-container {
292
- margin: 20px 0;
293
- display: flex;
294
- flex-wrap: wrap;
295
- gap: 10px;
296
- justify-content: center;
297
- }
298
- .search-container {
299
- margin: 20px 0;
300
- text-align: center;
301
- }
302
- #search-input {
303
- width: 90%;
304
- max-width: 600px;
305
- padding: 12px 18px;
306
- font-size: 1rem;
307
- border: 1px solid var(--border-color-light);
308
- border-radius: 25px;
309
- outline: none;
310
- box-shadow: inset 0 1px 3px rgba(0,0,0,0.05);
311
- transition: all 0.3s ease;
312
- background-color: var(--card-background-light);
313
- color: var(--text-color-light);
314
- }
315
- body.dark-mode #search-input {
316
- border-color: var(--border-color-dark);
317
- background-color: var(--card-background-dark);
318
- color: var(--text-color-dark);
319
- }
320
- #search-input:focus {
321
- border-color: var(--primary-color);
322
- box-shadow: 0 0 8px rgba(59, 130, 246, 0.3);
323
- }
324
- .category-filter {
325
- padding: 10px 20px;
326
- border: 1px solid var(--border-color-light);
327
- border-radius: 25px;
328
- background-color: var(--card-background-light);
329
- cursor: pointer;
330
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
331
- font-size: 0.95rem;
332
- font-weight: 500;
333
- color: var(--text-color-light);
334
- box-shadow: var(--shadow-light);
335
- }
336
- body.dark-mode .category-filter {
337
- border-color: var(--border-color-dark);
338
- background-color: var(--card-background-dark);
339
- color: var(--text-color-dark);
340
- box-shadow: var(--shadow-dark);
341
- }
342
- .category-filter.active, .category-filter:hover {
343
- background-color: var(--primary-color);
344
- color: white;
345
- border-color: var(--primary-color);
346
- box-shadow: var(--shadow-hover-light);
347
- }
348
- body.dark-mode .category-filter.active, body.dark-mode .category-filter:hover {
349
- background-color: var(--primary-color);
350
- border-color: var(--primary-color);
351
- box-shadow: var(--shadow-hover-dark);
352
- }
353
-
354
- .products-grid {
355
- display: grid;
356
- grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
357
- gap: 20px;
358
- padding: 10px;
359
- }
360
- .product {
361
- background: var(--card-background-light);
362
- border-radius: 18px;
363
- padding: 15px;
364
- box-shadow: var(--shadow-light);
365
- transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.3s ease;
366
- overflow: hidden;
367
- display: flex;
368
- flex-direction: column;
369
- justify-content: space-between;
370
- }
371
- body.dark-mode .product {
372
- background: var(--card-background-dark);
373
- box-shadow: var(--shadow-dark);
374
- }
375
- .product:hover {
376
- transform: translateY(-8px) scale(1.02);
377
- box-shadow: var(--shadow-hover-light);
378
- }
379
- body.dark-mode .product:hover {
380
- box-shadow: var(--shadow-hover-dark);
381
- }
382
- .product-image {
383
- width: 100%;
384
- aspect-ratio: 1;
385
- background-color: #f0f0f0;
386
- border-radius: 12px;
387
- overflow: hidden;
388
- display: flex;
389
- justify-content: center;
390
- align-items: center;
391
- margin-bottom: 10px;
392
- }
393
- body.dark-mode .product-image {
394
- background-color: #3a4250;
395
- }
396
- .product-image img {
397
- max-width: 100%;
398
- max-height: 100%;
399
- object-fit: contain;
400
- transition: transform 0.3s ease;
401
- }
402
- .product-image img:hover {
403
- transform: scale(1.05);
404
- }
405
- .product h2 {
406
- font-size: 1.05rem;
407
- font-weight: 600;
408
- margin: 5px 0;
409
- text-align: center;
410
- white-space: nowrap;
411
- overflow: hidden;
412
- text-overflow: ellipsis;
413
- color: var(--text-color-light);
414
- }
415
- body.dark-mode .product h2 {
416
- color: var(--text-color-dark);
417
- }
418
- .product-price {
419
- font-size: 1.15rem;
420
- color: var(--danger-color);
421
- font-weight: 700;
422
- text-align: center;
423
- margin: 5px 0 10px;
424
- }
425
- .product-description {
426
- font-size: 0.85rem;
427
- color: var(--secondary-text-color-light);
428
- text-align: center;
429
- margin-bottom: 15px;
430
- overflow: hidden;
431
- text-overflow: ellipsis;
432
- white-space: nowrap;
433
- }
434
- body.dark-mode .product-description {
435
- color: var(--secondary-text-color-dark);
436
- }
437
- .product-actions {
438
- display: flex;
439
- flex-direction: column;
440
- gap: 8px;
441
- margin-top: auto;
442
- }
443
- .product-button {
444
- display: block;
445
- width: 100%;
446
- padding: 10px;
447
- border: none;
448
- border-radius: 10px;
449
- background-color: var(--primary-color);
450
- color: white;
451
- font-size: 0.9rem;
452
- font-weight: 500;
453
- cursor: pointer;
454
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
455
- text-align: center;
456
- text-decoration: none;
457
- }
458
- .product-button:hover {
459
- background-color: var(--primary-dark-color);
460
- box-shadow: 0 4px 15px rgba(59, 130, 246, 0.4);
461
- }
462
- .add-to-cart {
463
- background-color: var(--accent-color);
464
- }
465
- .add-to-cart:hover {
466
- background-color: var(--accent-dark-color);
467
- box-shadow: 0 4px 15px rgba(16, 185, 129, 0.4);
468
- }
469
- #cart-button {
470
- position: fixed;
471
- bottom: 25px;
472
- right: 25px;
473
- background-color: var(--danger-color);
474
- color: white;
475
- border: none;
476
- border-radius: 50%;
477
- width: 55px;
478
- height: 55px;
479
- font-size: 1.3rem;
480
- cursor: pointer;
481
- display: none;
482
- box-shadow: 0 5px 20px rgba(239, 68, 68, 0.4);
483
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
484
- z-index: 1000;
485
- display: flex;
486
- align-items: center;
487
- justify-content: center;
488
- }
489
- #cart-button:hover {
490
- background-color: var(--danger-dark-color);
491
- transform: translateY(-3px) scale(1.05);
492
- box-shadow: 0 8px 25px rgba(239, 68, 68, 0.6);
493
- }
494
- .cart-count {
495
- position: absolute;
496
- top: -5px;
497
- right: -5px;
498
- background-color: var(--accent-color);
499
- color: white;
500
- border-radius: 50%;
501
- padding: 3px 7px;
502
- font-size: 0.75rem;
503
- min-width: 20px;
504
- text-align: center;
505
- font-weight: 600;
506
- }
507
-
508
- .modal {
509
- display: none;
510
- position: fixed;
511
- z-index: 1001;
512
- left: 0;
513
- top: 0;
514
- width: 100%;
515
- height: 100%;
516
- background-color: rgba(0,0,0,0.6);
517
- backdrop-filter: blur(8px);
518
- overflow-y: auto;
519
- padding: 20px;
520
- }
521
- .modal-content {
522
- background: var(--card-background-light);
523
- margin: 50px auto;
524
- padding: 30px;
525
- border-radius: 20px;
526
- width: 95%;
527
- max-width: 750px;
528
- box-shadow: 0 15px 40px rgba(0,0,0,0.3);
529
- animation: fadeInScale 0.3s ease-out;
530
- max-height: calc(100vh - 100px);
531
- overflow-y: auto;
532
- -webkit-overflow-scrolling: touch;
533
- position: relative;
534
- }
535
- body.dark-mode .modal-content {
536
- background: var(--card-background-dark);
537
- color: var(--text-color-dark);
538
- box-shadow: 0 15px 40px rgba(0,0,0,0.5);
539
- }
540
- @keyframes fadeInScale {
541
- from { opacity: 0; transform: translateY(-30px) scale(0.95); }
542
- to { opacity: 1; transform: translateY(0) scale(1); }
543
- }
544
- .close {
545
- position: absolute;
546
- top: 15px;
547
- right: 20px;
548
- font-size: 1.8rem;
549
- color: var(--secondary-text-color-light);
550
- cursor: pointer;
551
- transition: color 0.3s, transform 0.2s;
552
- }
553
- .close:hover {
554
- color: var(--danger-color);
555
- transform: rotate(90deg);
556
- }
557
- body.dark-mode .close {
558
- color: var(--secondary-text-color-dark);
559
- }
560
- body.dark-mode .close:hover {
561
- color: var(--danger-color);
562
- }
563
-
564
- .modal h2 {
565
- font-size: 1.6rem;
566
- font-weight: 700;
567
- margin-bottom: 20px;
568
- text-align: center;
569
- }
570
- .cart-item {
571
- display: flex;
572
- align-items: center;
573
- padding: 15px 0;
574
- border-bottom: 1px solid var(--border-color-light);
575
- }
576
- body.dark-mode .cart-item {
577
- border-bottom-color: var(--border-color-dark);
578
- }
579
- .cart-item:last-child {
580
- border-bottom: none;
581
- }
582
- .cart-item img {
583
- width: 60px;
584
- height: 60px;
585
- object-fit: contain;
586
- border-radius: 10px;
587
- margin-right: 15px;
588
- background-color: #f0f0f0;
589
- }
590
- body.dark-mode .cart-item img {
591
- background-color: #3a4250;
592
- }
593
- .cart-item-details {
594
- flex-grow: 1;
595
- }
596
- .cart-item-details strong {
597
- font-size: 1.1rem;
598
- font-weight: 600;
599
- }
600
- .cart-item-details p {
601
- font-size: 0.9rem;
602
- color: var(--secondary-text-color-light);
603
- }
604
- body.dark-mode .cart-item-details p {
605
- color: var(--secondary-text-color-dark);
606
- }
607
- .cart-item-total {
608
- font-size: 1rem;
609
- font-weight: 700;
610
- color: var(--danger-color);
611
- }
612
- .quantity-input, .color-select, .model-select {
613
- width: 100%;
614
- padding: 10px;
615
- border: 1px solid var(--border-color-light);
616
- border-radius: 8px;
617
- font-size: 1rem;
618
- margin: 8px 0;
619
- background-color: var(--card-background-light);
620
- color: var(--text-color-light);
621
- }
622
- body.dark-mode .quantity-input, body.dark-mode .color-select, body.dark-mode .model-select {
623
- border-color: var(--border-color-dark);
624
- background-color: var(--card-background-dark);
625
- color: var(--text-color-dark);
626
- }
627
- .modal-buttons {
628
- margin-top: 20px;
629
- display: flex;
630
- justify-content: flex-end;
631
- gap: 10px;
632
- }
633
- .modal-buttons .product-button {
634
- width: auto;
635
- padding: 10px 20px;
636
- }
637
- .clear-cart {
638
- background-color: var(--danger-color);
639
- }
640
- .clear-cart:hover {
641
- background-color: var(--danger-dark-color);
642
- box-shadow: 0 4px 15px rgba(239, 68, 68, 0.4);
643
- }
644
- .order-button {
645
- background-color: var(--accent-color);
646
- }
647
- .order-button:hover {
648
- background-color: var(--accent-dark-color);
649
- box-shadow: 0 4px 15px rgba(16, 185, 129, 0.4);
650
- }
651
-
652
- .swiper-container {
653
- max-width: 400px;
654
- margin: 0 auto 20px;
655
- border-radius: 15px;
656
- overflow: hidden;
657
- box-shadow: 0 5px 20px rgba(0,0,0,0.1);
658
- }
659
- body.dark-mode .swiper-container {
660
- box-shadow: 0 5px 20px rgba(0,0,0,0.3);
661
- }
662
- .swiper-slide {
663
- background-color: #f0f0f0;
664
- display: flex;
665
- justify-content: center;
666
- align-items: center;
667
- min-height: 250px;
668
- }
669
- body.dark-mode .swiper-slide {
670
- background-color: #3a4250;
671
- }
672
- .swiper-slide img {
673
- max-width: 100%;
674
- max-height: 300px;
675
- object-fit: contain;
676
- }
677
- .swiper-button-next, .swiper-button-prev {
678
- color: var(--primary-color) !important;
679
- background-color: rgba(255,255,255,0.8);
680
- border-radius: 50%;
681
- width: 40px;
682
- height: 40px;
683
- display: flex;
684
- align-items: center;
685
- justify-content: center;
686
- transition: background-color 0.3s;
687
- }
688
- .swiper-button-next:hover, .swiper-button-prev:hover {
689
- background-color: rgba(255,255,255,1);
690
- }
691
- body.dark-mode .swiper-button-next, body.dark-mode .swiper-button-prev {
692
- background-color: rgba(45, 55, 72, 0.8);
693
- }
694
- body.dark-mode .swiper-button-next:hover, body.dark-mode .swiper-button-prev:hover {
695
- background-color: rgba(45, 55, 72, 1);
696
- }
697
- .swiper-pagination-bullet {
698
- background-color: var(--primary-color) !important;
699
- }
700
-
701
- @media (max-width: 768px) {
702
- .header h1 {
703
- font-size: 1.4rem;
704
- }
705
- .category-filter {
706
- font-size: 0.85rem;
707
- padding: 8px 15px;
708
- }
709
- .products-grid {
710
- grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
711
- gap: 15px;
712
- }
713
- .product h2 {
714
- font-size: 0.95rem;
715
- }
716
- .product-price {
717
- font-size: 1.05rem;
718
- }
719
- .product-description {
720
- font-size: 0.75rem;
721
- }
722
- .product-button {
723
- font-size: 0.85rem;
724
- padding: 8px;
725
- }
726
- #cart-button {
727
- width: 45px;
728
- height: 45px;
729
- font-size: 1.1rem;
730
- bottom: 15px;
731
- right: 15px;
732
- }
733
- .modal-content {
734
- margin: 20px auto;
735
- padding: 20px;
736
- max-height: calc(100vh - 40px);
737
- }
738
- .close {
739
- font-size: 1.5rem;
740
- top: 10px;
741
- right: 15px;
742
- }
743
- .modal h2 {
744
- font-size: 1.4rem;
745
- }
746
- .cart-item img {
747
- width: 45px;
748
- height: 45px;
749
- }
750
- }
751
- </style>
752
- </head>
753
- <body>
754
- <div class="container">
755
- <div class="header">
756
- <div class="header-info">
757
- <img src="''' + LOGO_URL + '''" alt="Logo" class="header-logo">
758
- <h1>Каталог ZZIRIX</h1>
759
- </div>
760
- <button class="theme-toggle" onclick="toggleTheme()">
761
- <i class="fas fa-moon"></i>
762
- </button>
763
- </div>
764
- {% with messages = get_flashed_messages(with_categories=true) %}
765
- {% if messages %}
766
- {% for category, message in messages %}
767
- <div class="flash {{ category }}">{{ message }}</div>
768
- {% endfor %}
769
- {% endif %}
770
- {% endwith %}
771
- <div class="filters-container">
772
- <button class="category-filter active" data-category="all">Все категории</button>
773
- {% for category in categories %}
774
- <button class="category-filter" data-category="{{ category | escape }}">{{ category | escape }}</button>
775
- {% endfor %}
776
- </div>
777
- <div class="search-container">
778
- <input type="text" id="search-input" placeholder="Поиск товаров...">
779
- </div>
780
- <div class="products-grid" id="products-grid">
781
- {% for product in products %}
782
- <div class="product"
783
- data-name="{{ product['name']|lower }}"
784
- data-description="{{ product['description']|lower }}"
785
- data-category="{{ product.get('category', 'Без категории')|lower }}">
786
- {% if product.get('photos') and product['photos']|length > 0 %}
787
- <div class="product-image">
788
- <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product['photos'][0] }}?r={{ random.randint(1,1000) }}"
789
- alt="{{ product['name'] }}"
790
- loading="lazy">
791
- </div>
792
- {% else %}
793
- <div class="product-image">
794
- <img src="https://via.placeholder.com/200x200?text=No+Image"
795
- alt="Нет изображения"
796
- loading="lazy">
797
- </div>
798
- {% endif %}
799
- <h2>{{ product['name'] | escape }}</h2>
800
- <div class="product-price">{{ "%.2f"|format(product['price']) }} с</div>
801
- <p class="product-description">{{ product['description'][:50] | escape }}{% if product['description']|length > 50 %}...{% endif %}</p>
802
- <div class="product-actions">
803
- <button class="product-button" onclick="openModal('productModal', {{ loop.index0 }})">Подробнее</button>
804
- <button class="product-button add-to-cart" onclick="openModal('quantityModal', {{ loop.index0 }})">В корзину</button>
805
- </div>
806
- </div>
807
- {% endfor %}
808
- </div>
809
- </div>
810
-
811
- <div id="productModal" class="modal">
812
- <div class="modal-content">
813
- <span class="close" onclick="closeModal('productModal')">×</span>
814
- <div id="modalContent"></div>
815
- </div>
816
- </div>
817
-
818
- <div id="quantityModal" class="modal">
819
- <div class="modal-content">
820
- <span class="close" onclick="closeModal('quantityModal')">×</span>
821
- <h2>Укажите количество, цвет и модель</h2>
822
- <input type="number" id="quantityInput" class="quantity-input" min="1" value="1">
823
- <select id="colorSelect" class="color-select"></select>
824
- <select id="modelSelect" class="model-select"></select>
825
- <div class="modal-buttons">
826
- <button class="product-button order-button" onclick="confirmAddToCart()">Добавить</button>
827
- </div>
828
- </div>
829
- </div>
830
-
831
- <div id="cartModal" class="modal">
832
- <div class="modal-content">
833
- <span class="close" onclick="closeModal('cartModal')">×</span>
834
- <h2>Корзина</h2>
835
- <div id="cartContent"></div>
836
- <div style="margin-top: 20px; text-align: right;">
837
- <strong style="font-size: 1.2rem;">Итого: <span id="cartTotal">0</span> с</strong>
838
- <div class="modal-buttons">
839
- <button class="product-button clear-cart" onclick="clearCart()">Очистить</button>
840
- <button class="product-button order-button" onclick="orderViaWhatsApp()">Заказать</button>
841
- </div>
842
- </div>
843
- </div>
844
- </div>
845
-
846
- <button id="cart-button" onclick="openCartModal()">
847
- <i class="fas fa-shopping-cart"></i>
848
- <span id="cart-count" class="cart-count">0</span>
849
- </button>
850
-
851
- <script src="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.js"></script>
852
- <script>
853
- const products = {{ products|tojson }};
854
- let selectedProductIndex = null;
855
-
856
- function toggleTheme() {
857
- document.body.classList.toggle('dark-mode');
858
- const icon = document.querySelector('.theme-toggle i');
859
- icon.classList.toggle('fa-moon');
860
- icon.classList.toggle('fa-sun');
861
- localStorage.setItem('theme', document.body.classList.contains('dark-mode') ? 'dark' : 'light');
862
- }
863
-
864
- if (localStorage.getItem('theme') === 'dark') {
865
- document.body.classList.add('dark-mode');
866
- document.querySelector('.theme-toggle i').classList.replace('fa-moon', 'fa-sun');
867
- } else {
868
- document.querySelector('.theme-toggle i').classList.replace('fa-sun', 'fa-moon');
869
- }
870
-
871
- function openModal(modalId, index) {
872
- if (modalId === 'productModal') {
873
- selectedProductIndex = index;
874
- loadProductDetails(index);
875
- } else if (modalId === 'quantityModal') {
876
- selectedProductIndex = index;
877
- populateQuantityModal(index);
878
- }
879
- document.getElementById(modalId).style.display = "block";
880
- }
881
-
882
- function closeModal(modalId) {
883
- document.getElementById(modalId).style.display = "none";
884
- }
885
-
886
- function loadProductDetails(index) {
887
- fetch('/product_modal_content/' + index)
888
- .then(response => response.text())
889
- .then(data => {
890
- document.getElementById('modalContent').innerHTML = data;
891
- initializeSwiper();
892
- })
893
- .catch(error => console.error('Ошибка при загрузке деталей продукта:', error));
894
- }
895
-
896
- function initializeSwiper() {
897
- const swiper = new Swiper('.swiper-container', {
898
- slidesPerView: 1,
899
- spaceBetween: 20,
900
- loop: true,
901
- grabCursor: true,
902
- pagination: { el: '.swiper-pagination', clickable: true },
903
- navigation: { nextEl: '.swiper-button-next', prevEl: '.swiper-button-prev' },
904
- zoom: { maxRatio: 3 },
905
- on: {
906
- init: function () {
907
- this.slides.forEach(slide => {
908
- const img = slide.querySelector('img');
909
- if (img && !img.complete) {
910
- img.onload = () => swiper.update();
911
- }
912
- });
913
- },
914
- resize: function() {
915
- this.update();
916
- }
917
- }
918
- });
919
- }
920
-
921
- function populateQuantityModal(index) {
922
- selectedProductIndex = index;
923
- const product = products[index];
924
-
925
- const colorSelect = document.getElementById('colorSelect');
926
- colorSelect.innerHTML = '';
927
- if (product.colors && product.colors.length > 0) {
928
- product.colors.forEach(color => {
929
- const option = document.createElement('option');
930
- option.value = color;
931
- option.text = color;
932
- colorSelect.appendChild(option);
933
- });
934
- } else {
935
- const option = document.createElement('option');
936
- option.value = 'Не указан';
937
- option.text = 'Цвет не указан';
938
- colorSelect.appendChild(option);
939
- }
940
-
941
- const modelSelect = document.getElementById('modelSelect');
942
- modelSelect.innerHTML = '';
943
- if (product.models && product.models.length > 0) {
944
- product.models.forEach(model => {
945
- const option = document.createElement('option');
946
- option.value = model;
947
- option.text = model;
948
- modelSelect.appendChild(option);
949
- });
950
- } else {
951
- const option = document.createElement('option');
952
- option.value = 'Не указана';
953
- option.text = 'Модель не указана';
954
- modelSelect.appendChild(option);
955
- }
956
-
957
- document.getElementById('quantityInput').value = 1;
958
- }
959
-
960
- function confirmAddToCart() {
961
- if (selectedProductIndex === null) return;
962
- const quantityInput = document.getElementById('quantityInput');
963
- const quantity = parseInt(quantityInput.value) || 1;
964
- const color = document.getElementById('colorSelect').value;
965
- const model = document.getElementById('modelSelect').value;
966
-
967
- if (quantity <= 0) {
968
- alert("Укажите количество больше 0.");
969
- quantityInput.focus();
970
- return;
971
- }
972
-
973
- let cart = JSON.parse(localStorage.getItem('cart') || '[]');
974
- const product = products[selectedProductIndex];
975
- const cartItemId = `${product.id}-${color}-${model}`;
976
- const existingItem = cart.find(item => item.id === cartItemId);
977
-
978
- if (existingItem) {
979
- existingItem.quantity += quantity;
980
- } else {
981
- cart.push({
982
- id: cartItemId,
983
- product_id: product.id,
984
- name: product.name,
985
- price: product.price,
986
- photo: product.photos && product.photos.length > 0 ? product.photos[0] : '',
987
- quantity: quantity,
988
- color: color,
989
- model: model
990
- });
991
- }
992
-
993
- localStorage.setItem('cart', JSON.stringify(cart));
994
- closeModal('quantityModal');
995
- updateCartButton();
996
- }
997
-
998
- function updateCartButton() {
999
- const cart = JSON.parse(localStorage.getItem('cart') || '[]');
1000
- const cartCount = cart.reduce((sum, item) => sum + item.quantity, 0);
1001
- document.getElementById('cart-count').textContent = cartCount;
1002
- document.getElementById('cart-button').style.display = cartCount > 0 ? 'flex' : 'none';
1003
- }
1004
-
1005
- function openCartModal() {
1006
- const cart = JSON.parse(localStorage.getItem('cart') || '[]');
1007
- const cartContent = document.getElementById('cartContent');
1008
- let total = 0;
1009
-
1010
- cartContent.innerHTML = cart.length === 0 ? '<p style="text-align: center; color: var(--secondary-text-color-light);">Корзина пуста</p>' : cart.map(item => {
1011
- const itemTotal = item.price * item.quantity;
1012
- total += itemTotal;
1013
- const photoSrc = item.photo ? `https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/${item.photo}?r={{ random.randint(1,1000) }}` : 'https://via.placeholder.com/60x60?text=No+Image';
1014
- return `
1015
- <div class="cart-item">
1016
- <img src="${photoSrc}" alt="${item.name}">
1017
- <div class="cart-item-details">
1018
- <strong>${item.name}</strong>
1019
- <p>${item.price.toFixed(2)} с × ${item.quantity} (Цвет: ${item.color}, Модель: ${item.model})</p>
1020
- </div>
1021
- <span class="cart-item-total">${itemTotal.toFixed(2)} с</span>
1022
- </div>
1023
- `;
1024
- }).join('');
1025
-
1026
- document.getElementById('cartTotal').textContent = total.toFixed(2);
1027
- document.getElementById('cartModal').style.display = 'block';
1028
- }
1029
-
1030
- function orderViaWhatsApp() {
1031
- const cart = JSON.parse(localStorage.getItem('cart') || '[]');
1032
- if (cart.length === 0) {
1033
- alert("Корзина пуста!");
1034
- return;
1035
- }
1036
- let total = 0;
1037
- let orderText = "Здравствуйте, меня интересует заказ:%0A%0A";
1038
- cart.forEach((item, index) => {
1039
- const itemTotal = item.price * item.quantity;
1040
- total += itemTotal;
1041
- orderText += `${index + 1}. ${item.name}%0AКоличество: ${item.quantity}%0AЦена: ${item.price.toFixed(2)} с%0AЦвет: ${item.color}%0AМодель: ${item.model}%0A--%0A`;
1042
- });
1043
- orderText += `*Итого к оплате: ${total.toFixed(2)} с*%0A%0AЖду подтверждения заказа.`;
1044
- window.open(`https://api.whatsapp.com/send?phone=996705665777&text=${orderText}`, '_blank');
1045
- }
1046
-
1047
- function clearCart() {
1048
- localStorage.removeItem('cart');
1049
- openCartModal();
1050
- updateCartButton();
1051
- }
1052
-
1053
- window.onclick = function(event) {
1054
- if (event.target.classList.contains('modal')) {
1055
- event.target.style.display = "none";
1056
- }
1057
- }
1058
-
1059
- document.getElementById('search-input').addEventListener('input', filterProducts);
1060
- document.querySelectorAll('.category-filter').forEach(filter => {
1061
- filter.addEventListener('click', function() {
1062
- document.querySelectorAll('.category-filter').forEach(f => f.classList.remove('active'));
1063
- this.classList.add('active');
1064
- filterProducts();
1065
- });
1066
- });
1067
-
1068
- function filterProducts() {
1069
- const searchTerm = document.getElementById('search-input').value.toLowerCase().trim();
1070
- const activeCategory = document.querySelector('.category-filter.active').dataset.category.toLowerCase();
1071
- document.querySelectorAll('.product').forEach(product => {
1072
- const name = product.getAttribute('data-name');
1073
- const description = product.getAttribute('data-description');
1074
- const category = product.getAttribute('data-category');
1075
-
1076
- const matchesSearch = (name && name.includes(searchTerm)) || (description && description.includes(searchTerm));
1077
- const matchesCategory = activeCategory === 'all' || category === activeCategory;
1078
-
1079
- product.style.display = matchesSearch && matchesCategory ? 'flex' : 'none';
1080
- });
1081
- }
1082
-
1083
- updateCartButton();
1084
- </script>
1085
- </body>
1086
- </html>
1087
- '''
1088
- return render_template_string(catalog_html, products=products, categories=categories, repo_id=REPO_ID, LOGO_URL=LOGO_URL, random=random)
1089
-
1090
- @app.route('/product_modal_content/<int:index>')
1091
- def product_modal_content(index):
1092
- data = load_data()
1093
- products = data['products']
1094
- try:
1095
- product = products[index]
1096
- except IndexError:
1097
- return "Продукт не найден", 404
1098
- detail_html = '''
1099
- <div class="product-detail-modal" style="padding: 20px;">
1100
- <h2 style="font-size: 1.8rem; font-weight: 700; margin-bottom: 25px; text-align: center;">{{ product['name'] | escape }}</h2>
1101
- <div class="swiper-container" style="max-width: 450px; margin: 0 auto 30px;">
1102
- <div class="swiper-wrapper">
1103
- {% if product.get('photos') %}
1104
- {% for photo in product['photos'] %}
1105
- <div class="swiper-slide swiper-zoom-container" style="background-color: #f0f0f0; display: flex; justify-content: center; align-items: center; border-radius: 10px;">
1106
- <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo }}?r={{ random.randint(1,1000) }}"
1107
- alt="{{ product['name'] | escape }}"
1108
- style="max-width: 100%; max-height: 300px; object-fit: contain;">
1109
- </div>
1110
- {% endfor %}
1111
- {% else %}
1112
- <div class="swiper-slide swiper-zoom-container" style="background-color: #f0f0f0; display: flex; justify-content: center; align-items: center; border-radius: 10px;">
1113
- <img src="https://via.placeholder.com/300x300?text=No+Image" alt="No Image">
1114
- </div>
1115
- {% endif %}
1116
- </div>
1117
- <div class="swiper-pagination"></div>
1118
- <div class="swiper-button-next"></div>
1119
- <div class="swiper-button-prev"></div>
1120
- </div>
1121
- <p style="margin-bottom: 10px; font-size: 1rem;"><strong>Категория:</strong> <span style="color: var(--primary-color);">{{ product.get('category', 'Без категории') | escape }}</span></p>
1122
- <p style="margin-bottom: 10px; font-size: 1.1rem;"><strong>Цена:</strong> <span style="color: var(--danger-color); font-weight: 700;">{{ "%.2f"|format(product['price']) }} с</span></p>
1123
- <p style="margin-bottom: 15px; font-size: 1rem;"><strong>Описание:</strong> {{ product['description'] | escape }}</p>
1124
- <p style="margin-bottom: 10px; font-size: 0.95rem;"><strong>Доступные цвета:</strong> <span style="color: var(--secondary-text-color-light);">{{ product.get('colors', ['Нет цветов'])|join(', ') | escape }}</span></p>
1125
- <p style="margin-bottom: 10px; font-size: 0.95rem;"><strong>Доступные модели:</strong> <span style="color: var(--secondary-text-color-light);">{{ product.get('models', ['Нет моделей'])|join(', ') | escape }}</span></p>
1126
- </div>
1127
- '''
1128
- return render_template_string(detail_html, product=product, repo_id=REPO_ID, random=random)
1129
-
1130
- @app.route('/admin', methods=['GET', 'POST'])
1131
- def admin():
1132
- data = load_data()
1133
- products = data['products']
1134
- categories = sorted(data['categories'])
1135
-
1136
- if request.method == 'POST':
1137
- action = request.form.get('action')
1138
-
1139
- if action == 'add_category':
1140
- category_name = request.form.get('category_name', '').strip()
1141
- if category_name and category_name not in categories:
1142
- categories.append(category_name)
1143
- save_data(data)
1144
- flash('Категория успешно добавлена.', 'success')
1145
- return redirect(url_for('admin'))
1146
- flash('Ошибка: Категория уже существует или не указано название.', 'error')
1147
- return redirect(url_for('admin'))
1148
-
1149
- elif action == 'delete_category':
1150
- try:
1151
- category_index = int(request.form.get('category_index'))
1152
- except (TypeError, ValueError):
1153
- flash('Ошибка: Неверный индекс категории.', 'error')
1154
- return redirect(url_for('admin'))
1155
-
1156
- if 0 <= category_index < len(categories):
1157
- deleted_category = categories.pop(category_index)
1158
- for product in products:
1159
- if product.get('category') == deleted_category:
1160
- product['category'] = 'Без категории'
1161
- save_data(data)
1162
- flash('Категория удалена.', 'success')
1163
- return redirect(url_for('admin'))
1164
- flash('Ошибка: Категория не найдена.', 'error')
1165
- return redirect(url_for('admin'))
1166
-
1167
- elif action == 'add':
1168
- name = request.form.get('name', '').strip()
1169
- price = request.form.get('price', '').strip()
1170
- description = request.form.get('description', '').strip()
1171
- category = request.form.get('category', 'Без категории').strip()
1172
- photos_files = request.files.getlist('photos')
1173
- colors = [c.strip() for c in request.form.getlist('colors') if c.strip()]
1174
- models = [m.strip() for m in request.form.getlist('models') if m.strip()]
1175
- photos_list = []
1176
-
1177
- if not name or not price or not description:
1178
- flash('Ошибка: Заполните все обязательные поля (Название, Цена, Описание).', 'error')
1179
- return redirect(url_for('admin'))
1180
-
1181
- try:
1182
- price = float(price.replace(',', '.'))
1183
- except ValueError:
1184
- flash('Ошибка: Неверный формат цены.', 'error')
1185
- return redirect(url_for('admin'))
1186
-
1187
- # Change UPLOAD_FOLDER to /tmp for temporary file storage
1188
- UPLOAD_TEMP_DIR = '/tmp'
1189
- os.makedirs(UPLOAD_TEMP_DIR, exist_ok=True)
1190
-
1191
- if photos_files and any(f.filename for f in photos_files):
1192
- api = HfApi()
1193
- for i, photo in enumerate(photos_files[:10]):
1194
- if photo and photo.filename and allowed_file(photo.filename):
1195
- base, extension = os.path.splitext(photo.filename)
1196
- unique_filename = secure_filename(f"{name.replace(' ','_')}_{int(time.time())}_{i}{extension}")
1197
- temp_path = os.path.join(UPLOAD_TEMP_DIR, unique_filename)
1198
- try:
1199
- photo.save(temp_path)
1200
- api.upload_file(
1201
- path_or_fileobj=temp_path,
1202
- path_in_repo=f"photos/{unique_filename}",
1203
- repo_id=REPO_ID,
1204
- repo_type="dataset",
1205
- token=HF_TOKEN_WRITE,
1206
- commit_message=f"Добавлено фото для товара {name}"
1207
- )
1208
- photos_list.append(unique_filename)
1209
- except Exception as e:
1210
- logging.error(f"Ошибка при загрузке фото {unique_filename}: {e}")
1211
- flash(f'Ошибка при загрузке фото {photo.filename}: {e}', 'error')
1212
- finally:
1213
- if os.path.exists(temp_path):
1214
- os.remove(temp_path)
1215
-
1216
- new_product = {
1217
- 'id': str(uuid.uuid4()),
1218
- 'name': name,
1219
- 'price': price,
1220
- 'description': description,
1221
- 'category': category if category in categories else 'Без категории',
1222
- 'photos': photos_list,
1223
- 'colors': colors,
1224
- 'models': models
1225
- }
1226
- products.append(new_product)
1227
- save_data(data)
1228
- flash('Товар успешно добавлен.', 'success')
1229
- return redirect(url_for('admin'))
1230
-
1231
- elif action == 'edit':
1232
- try:
1233
- index = int(request.form.get('index'))
1234
- except (TypeError, ValueError):
1235
- flash('Ошибка: Неверный индекс товара для редактирования.', 'error')
1236
- return redirect(url_for('admin'))
1237
-
1238
- if not (0 <= index < len(products)):
1239
- flash('Ошибка: Товар не найден для редактирования.', 'error')
1240
- return redirect(url_for('admin'))
1241
-
1242
- name = request.form.get('name', '').strip()
1243
- price = request.form.get('price', '').strip()
1244
- description = request.form.get('description', '').strip()
1245
- category = request.form.get('category', 'Без категории').strip()
1246
- photos_files = request.files.getlist('photos')
1247
- colors = [c.strip() for c in request.form.getlist('colors') if c.strip()]
1248
- models = [m.strip() for m in request.form.getlist('models') if m.strip()]
1249
-
1250
- if not name or not price or not description:
1251
- flash('Ошибка: Заполните все обязательные поля (Название, Цена, Описание) при редактировании.', 'error')
1252
- return redirect(url_for('admin'))
1253
-
1254
- try:
1255
- price = float(price.replace(',', '.'))
1256
- except ValueError:
1257
- flash('Ошибка: Неверный формат цены при редактировании.', 'error')
1258
- return redirect(url_for('admin'))
1259
-
1260
- products[index]['name'] = name
1261
- products[index]['price'] = price
1262
- products[index]['description'] = description
1263
- products[index]['category'] = category if category in categories else 'Без категории'
1264
- products[index]['colors'] = colors
1265
- products[index]['models'] = models
1266
-
1267
- UPLOAD_TEMP_DIR = '/tmp'
1268
- os.makedirs(UPLOAD_TEMP_DIR, exist_ok=True)
1269
-
1270
- if photos_files and any(f.filename for f in photos_files):
1271
- new_photos_list = []
1272
- api = HfApi()
1273
- for i, photo in enumerate(photos_files[:10]):
1274
- if photo and photo.filename and allowed_file(photo.filename):
1275
- base, extension = os.path.splitext(photo.filename)
1276
- unique_filename = secure_filename(f"{name.replace(' ','_')}_{int(time.time())}_{i}{extension}")
1277
- temp_path = os.path.join(UPLOAD_TEMP_DIR, unique_filename)
1278
- try:
1279
- photo.save(temp_path)
1280
- api.upload_file(
1281
- path_or_fileobj=temp_path,
1282
- path_in_repo=f"photos/{unique_filename}",
1283
- repo_id=REPO_ID,
1284
- repo_type="dataset",
1285
- token=HF_TOKEN_WRITE,
1286
- commit_message=f"Обновлено фото для товара {name}"
1287
- )
1288
- new_photos_list.append(unique_filename)
1289
- except Exception as e:
1290
- logging.error(f"Ошибка при загрузке нового фото {unique_filename}: {e}")
1291
- flash(f'Ошибка при загрузке фото {photo.filename}: {e}', 'error')
1292
- finally:
1293
- if os.path.exists(temp_path):
1294
- os.remove(temp_path)
1295
- if new_photos_list:
1296
- products[index]['photos'] = new_photos_list
1297
-
1298
- save_data(data)
1299
- flash('Товар успешно обновлен.', 'success')
1300
- return redirect(url_for('admin'))
1301
-
1302
- elif action == 'delete':
1303
- try:
1304
- index = int(request.form.get('index'))
1305
- except (TypeError, ValueError):
1306
- flash('Ошибка: Неверный индекс товара для удаления.', 'error')
1307
- return redirect(url_for('admin'))
1308
-
1309
- if 0 <= index < len(products):
1310
- del products[index]
1311
- save_data(data)
1312
- flash('Товар удален.', 'success')
1313
- return redirect(url_for('admin'))
1314
- flash('Ошибка: Товар не найден для удаления.', 'error')
1315
- return redirect(url_for('admin'))
1316
-
1317
- admin_html = '''
1318
- <!DOCTYPE html>
1319
- <html lang="ru">
1320
- <head>
1321
- <meta charset="UTF-8">
1322
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
1323
- <title>Админ-панель ZZIRIX</title>
1324
- <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
1325
- <style>
1326
- :root {
1327
- --primary-color: #3B82F6;
1328
- --primary-dark-color: #2563eb;
1329
- --accent-color: #10b981;
1330
- --accent-dark-color: #059669;
1331
- --danger-color: #ef4444;
1332
- --danger-dark-color: #dc2626;
1333
- --background-light: linear-gradient(135deg, #f8f9fa, #e9ecef);
1334
- --background-dark: linear-gradient(135deg, #1a202c, #2d3748);
1335
- --card-background-light: #ffffff;
1336
- --card-background-dark: #2d3748;
1337
- --text-color-light: #2d3748;
1338
- --text-color-dark: #e2e8f0;
1339
- --secondary-text-color-light: #718096;
1340
- --secondary-text-color-dark: #a0aec0;
1341
- --border-color-light: #e2e8f0;
1342
- --border-color-dark: #4a5568;
1343
- --shadow-light: 0 6px 20px rgba(0, 0, 0, 0.08);
1344
- --shadow-hover-light: 0 10px 30px rgba(0, 0, 0, 0.15);
1345
- --shadow-dark: 0 6px 20px rgba(0, 0, 0, 0.25);
1346
- --shadow-hover-dark: 0 10px 30px rgba(0, 0, 0, 0.4);
1347
- }
1348
- body {
1349
- font-family: 'Roboto', sans-serif;
1350
- background: var(--background-light);
1351
- color: var(--text-color-light);
1352
- padding: 20px;
1353
- line-height: 1.6;
1354
- transition: background 0.3s, color 0.3s;
1355
- }
1356
- .container {
1357
- max-width: 1200px;
1358
- margin: 0 auto;
1359
- }
1360
- .header {
1361
- display: flex;
1362
- align-items: center;
1363
- padding: 15px 0;
1364
- border-bottom: 1px solid var(--border-color-light);
1365
- margin-bottom: 20px;
1366
- }
1367
- .header-logo {
1368
- width: 50px;
1369
- height: 50px;
1370
- border-radius: 50%;
1371
- object-fit: cover;
1372
- box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
1373
- transition: transform 0.3s ease, box-shadow 0.3s ease;
1374
- margin-right: 15px;
1375
- }
1376
- .header-logo:hover {
1377
- transform: scale(1.05);
1378
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
1379
- }
1380
- h1, h2 {
1381
- font-weight: 700;
1382
- margin-bottom: 25px;
1383
- color: var(--text-color-light);
1384
- }
1385
- body.dark-mode h1, body.dark-mode h2 {
1386
- color: var(--text-color-dark);
1387
- }
1388
- form {
1389
- background: var(--card-background-light);
1390
- padding: 30px;
1391
- border-radius: 20px;
1392
- box-shadow: var(--shadow-light);
1393
- margin-bottom: 30px;
1394
- transition: background 0.3s, box-shadow 0.3s;
1395
- }
1396
- body.dark-mode form {
1397
- background: var(--card-background-dark);
1398
- box-shadow: var(--shadow-dark);
1399
- }
1400
- label {
1401
- font-weight: 600;
1402
- margin-top: 18px;
1403
- margin-bottom: 5px;
1404
- display: block;
1405
- color: var(--text-color-light);
1406
- }
1407
- body.dark-mode label {
1408
- color: var(--text-color-dark);
1409
- }
1410
- input, textarea, select {
1411
- width: 100%;
1412
- padding: 12px;
1413
- margin-bottom: 10px;
1414
- border: 1px solid var(--border-color-light);
1415
- border-radius: 10px;
1416
- font-size: 1rem;
1417
- transition: all 0.3s ease;
1418
- background-color: var(--card-background-light);
1419
- color: var(--text-color-light);
1420
- }
1421
- body.dark-mode input, body.dark-mode textarea, body.dark-mode select {
1422
- border-color: var(--border-color-dark);
1423
- background-color: var(--card-background-dark);
1424
- color: var(--text-color-dark);
1425
- }
1426
- input:focus, textarea:focus, select:focus {
1427
- border-color: var(--primary-color);
1428
- box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
1429
- outline: none;
1430
- }
1431
- button {
1432
- padding: 12px 22px;
1433
- border: none;
1434
- border-radius: 10px;
1435
- background-color: var(--primary-color);
1436
- color: white;
1437
- font-weight: 500;
1438
- cursor: pointer;
1439
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
1440
- margin-top: 15px;
1441
- font-size: 1rem;
1442
- }
1443
- button:hover {
1444
- background-color: var(--primary-dark-color);
1445
- box-shadow: 0 4px 15px rgba(59, 130, 246, 0.4);
1446
- transform: translateY(-2px);
1447
- }
1448
- .delete-button {
1449
- background-color: var(--danger-color);
1450
- margin-left: 10px;
1451
- }
1452
- .delete-button:hover {
1453
- background-color: var(--danger-dark-color);
1454
- box-shadow: 0 4px 15px rgba(239, 68, 68, 0.4);
1455
- }
1456
- .product-list, .category-list {
1457
- display: grid;
1458
- gap: 20px;
1459
- }
1460
- .product-item, .category-item {
1461
- background: var(--card-background-light);
1462
- padding: 20px;
1463
- border-radius: 18px;
1464
- box-shadow: var(--shadow-light);
1465
- transition: background 0.3s, box-shadow 0.3s;
1466
- }
1467
- body.dark-mode .product-item, body.dark-mode .category-item {
1468
- background: var(--card-background-dark);
1469
- box-shadow: var(--shadow-dark);
1470
- }
1471
- .product-item h3, .category-item h3 {
1472
- font-size: 1.3rem;
1473
- font-weight: 600;
1474
- margin-bottom: 10px;
1475
- color: var(--text-color-light);
1476
- }
1477
- body.dark-mode .product-item h3, body.dark-mode .category-item h3 {
1478
- color: var(--text-color-dark);
1479
- }
1480
- .product-item p {
1481
- font-size: 0.95rem;
1482
- margin-bottom: 5px;
1483
- color: var(--secondary-text-color-light);
1484
- }
1485
- body.dark-mode .product-item p {
1486
- color: var(--secondary-text-color-dark);
1487
- }
1488
- .edit-form {
1489
- margin-top: 20px;
1490
- padding: 20px;
1491
- background: var(--background-light);
1492
- border-radius: 15px;
1493
- box-shadow: inset 0 2px 5px rgba(0,0,0,0.05);
1494
- }
1495
- body.dark-mode .edit-form {
1496
- background: #3a4250;
1497
- box-shadow: inset 0 2px 5px rgba(0,0,0,0.15);
1498
- }
1499
- .color-input-group, .model-input-group {
1500
- display: flex;
1501
- gap: 10px;
1502
- margin-bottom: 10px;
1503
- align-items: center;
1504
- }
1505
- .color-input-group input, .model-input-group input {
1506
- flex-grow: 1;
1507
- margin-bottom: 0;
1508
- }
1509
- .remove-input-btn {
1510
- background-color: #ccc;
1511
- color: #555;
1512
- padding: 8px 12px;
1513
- border-radius: 8px;
1514
- font-size: 0.8rem;
1515
- margin-top: 0;
1516
- flex-shrink: 0;
1517
- }
1518
- .remove-input-btn:hover {
1519
- background-color: #bbb;
1520
- box-shadow: none;
1521
- transform: none;
1522
- }
1523
- .add-color-btn, .add-model-btn {
1524
- background-color: var(--accent-color);
1525
- width: auto;
1526
- padding: 10px 18px;
1527
- margin-bottom: 15px;
1528
- }
1529
- .add-color-btn:hover, .add-model-btn:hover {
1530
- background-color: var(--accent-dark-color);
1531
- box-shadow: 0 4px 15px rgba(16, 185, 129, 0.4);
1532
- }
1533
- .product-photos-preview {
1534
- display: flex;
1535
- flex-wrap: wrap;
1536
- gap: 10px;
1537
- margin-top: 10px;
1538
- margin-bottom: 15px;
1539
- }
1540
- .product-photos-preview img {
1541
- max-width: 90px;
1542
- height: auto;
1543
- border-radius: 8px;
1544
- border: 1px solid var(--border-color-light);
1545
- box-shadow: 0 2px 8px rgba(0,0,0,0.05);
1546
- }
1547
- body.dark-mode .product-photos-preview img {
1548
- border-color: var(--border-color-dark);
1549
- box-shadow: 0 2px 8px rgba(0,0,0,0.15);
1550
- }
1551
-
1552
- .admin-section-title {
1553
- margin-top: 40px;
1554
- margin-bottom: 20px;
1555
- font-size: 1.8rem;
1556
- color: var(--primary-color);
1557
- }
1558
- body.dark-mode .admin-section-title {
1559
- color: var(--primary-color);
1560
- }
1561
-
1562
- .db-management-buttons button {
1563
- margin-right: 10px;
1564
- }
1565
-
1566
- .search-admin-container {
1567
- margin: 20px 0;
1568
- text-align: left;
1569
- }
1570
- #admin-search-input {
1571
- width: 100%;
1572
- max-width: 400px;
1573
- margin-left: 0;
1574
- }
1575
-
1576
- .flash {
1577
- padding: 15px;
1578
- margin-bottom: 20px;
1579
- border-radius: 8px;
1580
- font-weight: 500;
1581
- border: 1px solid transparent;
1582
- }
1583
- .flash.success {
1584
- background-color: #d1fae5;
1585
- color: #065f46;
1586
- border-color: #34d399;
1587
- }
1588
- .flash.error {
1589
- background-color: #fee2e2;
1590
- color: #991b1b;
1591
- border-color: #ef4444;
1592
- }
1593
- </style>
1594
- </head>
1595
- <body>
1596
- <div class="container">
1597
- <div class="header">
1598
- <div class="header-info">
1599
- <img src="''' + LOGO_URL + '''" alt="Logo" class="header-logo">
1600
- <h1>Админ-панель</h1>
1601
- </div>
1602
- </div>
1603
- {% with messages = get_flashed_messages(with_categories=true) %}
1604
- {% if messages %}
1605
- {% for category, message in messages %}
1606
- <div class="flash {{ category }}">{{ message }}</div>
1607
- {% endfor %}
1608
- {% endif %}
1609
- {% endwith %}
1610
-
1611
- <h2 class="admin-section-title">Добавление товара</h2>
1612
- <form method="POST" enctype="multipart/form-data">
1613
- <input type="hidden" name="action" value="add">
1614
- <label>Название товара:</label>
1615
- <input type="text" name="name" required>
1616
- <label>Цена:</label>
1617
- <input type="number" name="price" step="0.01" required>
1618
- <label>Описание:</label>
1619
- <textarea name="description" rows="4" required></textarea>
1620
- <label>Категория:</label>
1621
- <select name="category">
1622
- <option value="Без категории">Без категории</option>
1623
- {% for category in categories %}
1624
- <option value="{{ category | escape }}">{{ category | escape }}</option>
1625
- {% endfor %}
1626
- </select>
1627
- <label>Фотографии (до 10):</label>
1628
- <input type="file" name="photos" accept="image/*" multiple>
1629
- <label>Цвета:</label>
1630
- <div id="color-inputs">
1631
- <div class="color-input-group">
1632
- <input type="text" name="colors" placeholder="Например: Красный">
1633
- <button type="button" class="remove-input-btn" onclick="this.parentNode.remove()">X</button>
1634
- </div>
1635
- </div>
1636
- <button type="button" class="add-color-btn" onclick="addInput('color-inputs', 'colors', 'Цвет')">Добавить цвет</button>
1637
- <label>Модели телефона:</label>
1638
- <div id="model-inputs">
1639
- <div class="model-input-group">
1640
- <input type="text" name="models" placeholder="Например: iPhone 14">
1641
- <button type="button" class="remove-input-btn" onclick="this.parentNode.remove()">X</button>
1642
- </div>
1643
- </div>
1644
- <button type="button" class="add-model-btn" onclick="addInput('model-inputs', 'models', 'Модель')">Добавить модель</button>
1645
- <button type="submit">Добавить товар</button>
1646
- </form>
1647
-
1648
- <h2 class="admin-section-title">Управление категориями</h2>
1649
- <form method="POST">
1650
- <input type="hidden" name="action" value="add_category">
1651
- <label>Название категории:</label>
1652
- <input type="text" name="category_name" required>
1653
- <button type="submit">Добавить</button>
1654
- </form>
1655
-
1656
- <h2>Список категорий</h2>
1657
- <div class="category-list">
1658
- {% for category in categories %}
1659
- <div class="category-item">
1660
- <h3>{{ category | escape }}</h3>
1661
- <form method="POST" style="display: inline;">
1662
- <input type="hidden" name="action" value="delete_category">
1663
- <input type="hidden" name="category_index" value="{{ loop.index0 }}">
1664
- <button type="submit" class="delete-button">Удалить</button>
1665
- </form>
1666
- </div>
1667
- {% endfor %}
1668
- </div>
1669
-
1670
- <h2 class="admin-section-title">Управление базой данных</h2>
1671
- <div class="db-management-buttons">
1672
- <form method="POST" action="{{ url_for('backup') }}" style="display: inline;">
1673
- <button type="submit">Создать копию (вручную)</button>
1674
- </form>
1675
- <form method="GET" action="{{ url_for('download') }}" style="display: inline;">
1676
- <button type="submit">Скачать базу (JSON)</button>
1677
- </form>
1678
- </div>
1679
-
1680
- <h2 class="admin-section-title">Список товаров</h2>
1681
- <div class="search-admin-container">
1682
- <input type="text" id="admin-search-input" placeholder="Поиск товаров в админ-панели...">
1683
- </div>
1684
- <div class="product-list" id="admin-product-list">
1685
- {% for product in products %}
1686
- <div class="product-item"
1687
- data-name="{{ product['name']|lower }}"
1688
- data-description="{{ product['description']|lower }}"
1689
- data-category="{{ product.get('category', 'без категории')|lower }}">
1690
- <h3>{{ product['name'] | escape }}</h3>
1691
- <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') | escape }}</p>
1692
- <p><strong>Цена:</strong> {{ "%.2f"|format(product['price']) }} с</p>
1693
- <p><strong>Описание:</strong> {{ product['description'] | escape }}</p>
1694
- <p><strong>Цвета:</strong> {{ product.get('colors', ['Нет цветов'])|join(', ') | escape }}</p>
1695
- <p><strong>Модели:</strong> {{ product.get('models', ['Нет моделей'])|join(', ') | escape }}</p>
1696
- {% if product.get('photos') and product['photos']|length > 0 %}
1697
- <div class="product-photos-preview">
1698
- {% for photo in product['photos'] %}
1699
- <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo }}?r={{ random.randint(1,1000) }}"
1700
- alt="{{ product['name'] | escape }}"
1701
- loading="lazy">
1702
- {% endfor %}
1703
- </div>
1704
- {% endif %}
1705
- <details>
1706
- <summary style="font-weight: 500; cursor: pointer; color: var(--primary-color); margin-top: 15px;">Редактировать</summary>
1707
- <form method="POST" enctype="multipart/form-data" class="edit-form">
1708
- <input type="hidden" name="action" value="edit">
1709
- <input type="hidden" name="index" value="{{ loop.index0 }}">
1710
- <label>Название:</label>
1711
- <input type="text" name="name" value="{{ product['name'] | escape }}" required>
1712
- <label>Цена:</label>
1713
- <input type="number" name="price" step="0.01" value="{{ product['price'] }}" required>
1714
- <label>Описание:</label>
1715
- <textarea name="description" rows="4" required>{{ product['description'] | escape }}</textarea>
1716
- <label>Категория:</label>
1717
- <select name="category">
1718
- <option value="Без категории" {% if product.get('category', 'Без категории') == 'Без категории' %}selected{% endif %}>Без категории</option>
1719
- {% for category in categories %}
1720
- <option value="{{ category | escape }}" {% if product.get('category') == category %}selected{% endif %}>{{ category | escape }}</option>
1721
- {% endfor %}
1722
- </select>
1723
- <label>Фотографии (заменят существующие, до 10):</label>
1724
- <input type="file" name="photos" accept="image/*" multiple>
1725
- <label>Цвета:</label>
1726
- <div id="edit-color-inputs-{{ loop.index0 }}">
1727
- {% for color in product.get('colors', []) %}
1728
- <div class="color-input-group">
1729
- <input type="text" name="colors" value="{{ color | escape }}">
1730
- <button type="button" class="remove-input-btn" onclick="this.parentNode.remove()">X</button>
1731
- </div>
1732
- {% endfor %}
1733
- <div class="color-input-group">
1734
- <input type="text" name="colors" placeholder="Например: Новый цвет">
1735
- <button type="button" class="remove-input-btn" onclick="this.parentNode.remove()">X</button>
1736
- </div>
1737
- </div>
1738
- <button type="button" class="add-color-btn" onclick="addInput('edit-color-inputs-{{ loop.index0 }}', 'colors', 'Цвет')">Добавить цвет</button>
1739
- <label>Модели телефона:</label>
1740
- <div id="edit-model-inputs-{{ loop.index0 }}">
1741
- {% for model in product.get('models', []) %}
1742
- <div class="model-input-group">
1743
- <input type="text" name="models" value="{{ model | escape }}">
1744
- <button type="button" class="remove-input-btn" onclick="this.parentNode.remove()">X</button>
1745
- </div>
1746
- {% endfor %}
1747
- <div class="model-input-group">
1748
- <input type="text" name="models" placeholder="Например: Новая модель">
1749
- <button type="button" class="remove-input-btn" onclick="this.parentNode.remove()">X</button>
1750
- </div>
1751
- </div>
1752
- <button type="button" class="add-model-btn" onclick="addInput('edit-model-inputs-{{ loop.index0 }}', 'models', 'Модель')">Добавить модель</button>
1753
- <button type="submit">Сохранить</button>
1754
- </form>
1755
- </details>
1756
- <form method="POST" style="margin-top: 15px;">
1757
- <input type="hidden" name="action" value="delete">
1758
- <input type="hidden" name="index" value="{{ loop.index0 }}">
1759
- <button type="submit" class="delete-button">Удалить товар</button>
1760
- </form>
1761
- </div>
1762
- {% endfor %}
1763
- </div>
1764
- </div>
1765
- <script>
1766
- function addInput(containerId, name, placeholderPrefix) {
1767
- const container = document.getElementById(containerId);
1768
- const newInput = document.createElement('div');
1769
- newInput.className = `${name.replace('s', '')}-input-group`;
1770
- newInput.innerHTML = `
1771
- <input type="text" name="${name}" placeholder="Например: ${placeholderPrefix}">
1772
- <button type="button" class="remove-input-btn" onclick="this.parentNode.remove()">X</button>
1773
- `;
1774
- container.appendChild(newInput);
1775
- }
1776
-
1777
- document.getElementById('admin-search-input').addEventListener('input', filterAdminProducts);
1778
-
1779
- function filterAdminProducts() {
1780
- const searchTerm = document.getElementById('admin-search-input').value.toLowerCase().trim();
1781
- document.querySelectorAll('.product-item').forEach(productItem => {
1782
- const name = productItem.getAttribute('data-name');
1783
- const description = productItem.getAttribute('data-description');
1784
- const category = productItem.getAttribute('data-category');
1785
-
1786
- const matchesSearch = (name && name.includes(searchTerm)) ||
1787
- (description && description.includes(searchTerm)) ||
1788
- (category && category.includes(searchTerm));
1789
-
1790
- productItem.style.display = matchesSearch ? 'block' : 'none';
1791
- });
1792
- }
1793
- </script>
1794
- </body>
1795
- </html>
1796
- '''
1797
- return render_template_string(admin_html, products=products, categories=categories, repo_id=REPO_ID, LOGO_URL=LOGO_URL, random=random)
1798
-
1799
- @app.route('/backup', methods=['POST'])
1800
- def backup():
1801
- try:
1802
- upload_db_to_hf()
1803
- flash('Резервная копия успешно создана и загружена.', 'success')
1804
- return redirect(url_for('admin'))
1805
- except Exception as e:
1806
- logging.error(f"Ошибка при ручном создании резервной копии: {e}")
1807
- flash(f'Ошибка при создании резервной копии: {e}', 'error')
1808
- return redirect(url_for('admin'))
1809
-
1810
- @app.route('/download', methods=['GET'])
1811
- def download():
1812
- try:
1813
- download_db_from_hf()
1814
- if os.path.exists(DATA_FILE):
1815
- return send_file(DATA_FILE, as_attachment=True, mimetype='application/json', download_name='data_zzirix_backup.json')
1816
- flash('Файл базы данных не найден после попытки скачивания.', 'error')
1817
- return redirect(url_for('admin'))
1818
- except Exception as e:
1819
- logging.error(f"Ошибка при скачивании базы данных: {e}")
1820
- flash(f'Ошибка при скачивании базы данных: {e}', 'error')
1821
- return redirect(url_for('admin'))
1822
-
1823
- if __name__ == '__main__':
1824
- # UPLOAD_FOLDER is no longer used, but let's keep this if other parts need a generic 'uploads' folder
1825
- # For temporary file storage, we now use '/tmp' directly in the admin functions.
1826
- # uploads_dir = 'uploads'
1827
- # os.makedirs(uploads_dir, exist_ok=True)
1828
-
1829
- logging.info("Начальная проверка и обновление структуры данных...")
1830
- try:
1831
- current_data = load_data()
1832
- save_data(current_data)
1833
- logging.info("Структура данных проверена и при необходимости обновлена.")
1834
- except Exception as e:
1835
- logging.error(f"Ошибка во время начальной проверки/обновления структуры данных: {e}")
1836
-
1837
- backup_thread = threading.Thread(target=periodic_backup, daemon=True)
1838
- backup_thread.start()
1839
- logging.info("Запуск Flask приложения...")
1840
- app.run(debug=True, host='0.0.0.0', port=7860)