Kgshop commited on
Commit
4564286
·
verified ·
1 Parent(s): e9d3f19

Delete app.py

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