Kgshop commited on
Commit
ba0a25f
·
verified ·
1 Parent(s): 09f5b51

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +408 -524
app.py CHANGED
@@ -1,4 +1,3 @@
1
-
2
  from flask import Flask, render_template_string, request, redirect, url_for, flash, send_file
3
  import json
4
  import os
@@ -17,7 +16,7 @@ app = Flask(__name__)
17
  app.secret_key = os.getenv("FLASK_SECRET_KEY", "zzirix_secret_key_for_cart")
18
  DATA_FILE = 'data_zzirix.json'
19
 
20
- REPO_ID = "Kgshop/clients" # Using REPO_ID from app (2) (4).py for consistency with image paths in JSON
21
  HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
22
  HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") or HF_TOKEN_WRITE
23
 
@@ -33,7 +32,6 @@ def initialize_data_structure(data):
33
  data.setdefault('products', [])
34
  data.setdefault('orders', {})
35
 
36
- # Ensure "Без категории" exists if there are products with it
37
  if any(p.get('category') == 'Без категории' for p in data['products']) and 'Без категории' not in data['categories']:
38
  data['categories'].append('Без категории')
39
 
@@ -47,20 +45,16 @@ def initialize_data_structure(data):
47
  product.setdefault('price', 0.0)
48
  product.setdefault('colors', [])
49
  product.setdefault('models', [])
50
- product.setdefault('photos', []) # Use 'photos' as per the provided JSON structure
51
- product.setdefault('in_stock', True) # From provided JSON
52
- product.setdefault('is_top', False) # From provided JSON
53
 
54
- # For compatibility, if 'media' was used, convert it to 'photos' for new system
55
- # Or if 'photos' is empty, try to populate from 'media'
56
  if not product['photos'] and 'media' in product and product['media']:
57
  product['photos'] = [m['filename'] for m in product['media'] if m['type'] == 'photo']
58
 
59
- # Remove old 'media' field if it exists, as 'photos' is now canonical
60
  if 'media' in product:
61
  del product['media']
62
 
63
- # Sort categories to ensure "Без категории" is usually first or consistently placed
64
  data['categories'] = sorted(list(set(data['categories'])), key=lambda x: (x != 'Без категории', x))
65
 
66
  return data
@@ -175,8 +169,8 @@ BASE_STYLE = '''
175
  --accent-dark-color: #059669;
176
  --danger-color: #ef4444;
177
  --danger-dark-color: #dc2626;
178
- --background-light: linear-gradient(135deg, #f8f9fa, #e9ecef);
179
- --background-dark: linear-gradient(135deg, #1a202c, #2d3748);
180
  --card-background-light: #ffffff;
181
  --card-background-dark: #2d3748;
182
  --text-color-light: #2d3748;
@@ -185,10 +179,13 @@ BASE_STYLE = '''
185
  --secondary-text-color-dark: #a0aec0;
186
  --border-color-light: #e2e8f0;
187
  --border-color-dark: #4a5568;
188
- --shadow-light: 0 6px 20px rgba(0, 0, 0, 0.08);
189
- --shadow-hover-light: 0 10px 30px rgba(0, 0, 0, 0.15);
190
- --shadow-dark: 0 6px 20px rgba(0, 0, 0, 0.25);
191
- --shadow-hover-dark: 0 10px 30px rgba(0, 0, 0, 0.4);
 
 
 
192
  }
193
  * {
194
  margin: 0;
@@ -210,47 +207,53 @@ body.dark-mode {
210
  color: var(--text-color-dark);
211
  }
212
  .container {
213
- max-width: 1300px;
214
  margin: 0 auto;
215
- padding: 20px;
216
  flex-grow: 1;
217
  }
218
  .header {
219
  display: flex;
220
  justify-content: space-between;
221
  align-items: center;
222
- padding: 15px 0;
223
- border-bottom: 1px solid var(--border-color-light);
224
- margin-bottom: 20px;
 
 
 
225
  }
226
  body.dark-mode .header {
227
  border-bottom-color: var(--border-color-dark);
 
 
228
  }
229
  .header-info {
230
  display: flex;
231
  align-items: center;
232
  }
233
  .header-logo {
234
- width: 50px;
235
- height: 50px;
236
  border-radius: 50%;
237
  object-fit: cover;
238
- box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
239
  transition: transform 0.3s ease, box-shadow 0.3s ease;
 
240
  }
241
  .header-logo:hover {
242
- transform: scale(1.05);
243
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
244
  }
245
  .header h1 {
246
- font-size: 1.7rem;
247
- font-weight: 700;
248
- margin-left: 15px;
249
  }
250
  .theme-toggle {
251
  background: none;
252
  border: none;
253
- font-size: 1.6rem;
254
  cursor: pointer;
255
  color: var(--secondary-text-color-light);
256
  transition: color 0.3s ease, transform 0.2s ease;
@@ -260,16 +263,17 @@ body.dark-mode .theme-toggle {
260
  color: var(--secondary-text-color-dark);
261
  }
262
  .theme-toggle:hover {
263
- color: var(--primary-color);
264
- transform: rotate(15deg);
265
  }
266
 
267
  .flash {
268
- padding: 15px;
269
- margin-bottom: 20px;
270
- border-radius: 8px;
271
  font-weight: 500;
272
  border: 1px solid transparent;
 
273
  }
274
  .flash.success {
275
  background-color: var(--accent-color);
@@ -283,25 +287,25 @@ body.dark-mode .theme-toggle {
283
  }
284
 
285
  .filters-container {
286
- margin: 20px 0;
287
  display: flex;
288
  flex-wrap: wrap;
289
- gap: 10px;
290
  justify-content: center;
291
  }
292
  .search-container {
293
- margin: 20px 0;
294
  text-align: center;
295
  }
296
  #search-input {
297
  width: 90%;
298
  max-width: 600px;
299
- padding: 12px 18px;
300
  font-size: 1rem;
301
- border: 1px solid var(--border-color-light);
302
- border-radius: 25px;
303
  outline: none;
304
- box-shadow: inset 0 1px 3px rgba(0,0,0,0.05);
305
  transition: all 0.3s ease;
306
  background-color: var(--card-background-light);
307
  color: var(--text-color-light);
@@ -310,19 +314,20 @@ body.dark-mode #search-input {
310
  border-color: var(--border-color-dark);
311
  background-color: var(--card-background-dark);
312
  color: var(--text-color-dark);
 
313
  }
314
  #search-input:focus {
315
  border-color: var(--primary-color);
316
- box-shadow: 0 0 8px rgba(59, 130, 246, 0.3);
317
  }
318
  .category-filter {
319
- padding: 10px 20px;
320
- border: 1px solid var(--border-color-light);
321
- border-radius: 25px;
322
  background-color: var(--card-background-light);
323
  cursor: pointer;
324
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
325
- font-size: 0.95rem;
326
  font-weight: 500;
327
  color: var(--text-color-light);
328
  box-shadow: var(--shadow-light);
@@ -347,14 +352,14 @@ body.dark-mode .category-filter.active, body.dark-mode .category-filter:hover {
347
 
348
  .products-grid {
349
  display: grid;
350
- grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
351
- gap: 20px;
352
- padding: 10px;
353
  }
354
  .product {
355
  background: var(--card-background-light);
356
- border-radius: 18px;
357
- padding: 15px;
358
  box-shadow: var(--shadow-light);
359
  transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.3s ease;
360
  overflow: hidden;
@@ -367,7 +372,7 @@ body.dark-mode .product {
367
  box-shadow: var(--shadow-dark);
368
  }
369
  .product:hover {
370
- transform: translateY(-8px) scale(1.02);
371
  box-shadow: var(--shadow-hover-light);
372
  }
373
  body.dark-mode .product:hover {
@@ -376,30 +381,32 @@ body.dark-mode .product:hover {
376
  .product-image {
377
  width: 100%;
378
  aspect-ratio: 1;
379
- background-color: #f0f0f0;
380
- border-radius: 12px;
381
  overflow: hidden;
382
  display: flex;
383
  justify-content: center;
384
  align-items: center;
385
- margin-bottom: 10px;
 
386
  }
387
  body.dark-mode .product-image {
388
- background-color: #3a4250;
389
  }
390
  .product-image img, .product-image video {
391
  max-width: 100%;
392
  max-height: 100%;
393
  object-fit: contain;
394
  transition: transform 0.3s ease;
 
395
  }
396
  .product-image img:hover, .product-image video:hover {
397
  transform: scale(1.05);
398
  }
399
  .product h2 {
400
- font-size: 1.05rem;
401
- font-weight: 600;
402
- margin: 5px 0;
403
  text-align: center;
404
  white-space: nowrap;
405
  overflow: hidden;
@@ -410,17 +417,17 @@ body.dark-mode .product h2 {
410
  color: var(--text-color-dark);
411
  }
412
  .product-price {
413
- font-size: 1.15rem;
414
  color: var(--danger-color);
415
  font-weight: 700;
416
  text-align: center;
417
- margin: 5px 0 10px;
418
  }
419
  .product-description {
420
- font-size: 0.85rem;
421
  color: var(--secondary-text-color-light);
422
  text-align: center;
423
- margin-bottom: 15px;
424
  overflow: hidden;
425
  text-overflow: ellipsis;
426
  white-space: nowrap;
@@ -431,48 +438,51 @@ body.dark-mode .product-description {
431
  .product-actions {
432
  display: flex;
433
  flex-direction: column;
434
- gap: 8px;
435
  margin-top: auto;
436
  }
437
  .product-button {
438
  display: block;
439
  width: 100%;
440
- padding: 10px;
441
  border: none;
442
- border-radius: 10px;
443
  background-color: var(--primary-color);
444
  color: white;
445
- font-size: 0.9rem;
446
  font-weight: 500;
447
  cursor: pointer;
448
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
449
  text-align: center;
450
  text-decoration: none;
 
451
  }
452
  .product-button:hover {
453
  background-color: var(--primary-dark-color);
454
- box-shadow: 0 4px 15px rgba(59, 130, 246, 0.4);
 
455
  }
456
  .add-to-cart {
457
  background-color: var(--accent-color);
 
458
  }
459
  .add-to-cart:hover {
460
  background-color: var(--accent-dark-color);
461
- box-shadow: 0 4px 15px rgba(16, 185, 129, 0.4);
462
  }
463
  #cart-button {
464
  position: fixed;
465
- bottom: 25px;
466
- right: 25px;
467
  background-color: var(--danger-color);
468
  color: white;
469
  border: none;
470
  border-radius: 50%;
471
- width: 55px;
472
- height: 55px;
473
- font-size: 1.3rem;
474
  cursor: pointer;
475
- box-shadow: 0 5px 20px rgba(239, 68, 68, 0.4);
476
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
477
  z-index: 1000;
478
  display: flex;
@@ -481,8 +491,8 @@ body.dark-mode .product-description {
481
  }
482
  #cart-button:hover {
483
  background-color: var(--danger-dark-color);
484
- transform: translateY(-3px) scale(1.05);
485
- box-shadow: 0 8px 25px rgba(239, 68, 68, 0.6);
486
  }
487
  .cart-count {
488
  position: absolute;
@@ -491,9 +501,9 @@ body.dark-mode .product-description {
491
  background-color: var(--accent-color);
492
  color: white;
493
  border-radius: 50%;
494
- padding: 3px 7px;
495
- font-size: 0.75rem;
496
- min-width: 20px;
497
  text-align: center;
498
  font-weight: 600;
499
  }
@@ -514,13 +524,13 @@ body.dark-mode .product-description {
514
  .modal-content {
515
  background: var(--card-background-light);
516
  margin: 50px auto;
517
- padding: 30px;
518
- border-radius: 20px;
519
  width: 95%;
520
  max-width: 750px;
521
- box-shadow: 0 15px 40px rgba(0,0,0,0.3);
522
  animation: fadeInScale 0.3s ease-out;
523
- max-height: calc(100vh - 100px);
524
  overflow-y: auto;
525
  -webkit-overflow-scrolling: touch;
526
  position: relative;
@@ -528,7 +538,7 @@ body.dark-mode .product-description {
528
  body.dark-mode .modal-content {
529
  background: var(--card-background-dark);
530
  color: var(--text-color-dark);
531
- box-shadow: 0 15px 40px rgba(0,0,0,0.5);
532
  }
533
  @keyframes fadeInScale {
534
  from { opacity: 0; transform: translateY(-30px) scale(0.95); }
@@ -536,9 +546,9 @@ body.dark-mode .modal-content {
536
  }
537
  .close {
538
  position: absolute;
539
- top: 15px;
540
- right: 20px;
541
- font-size: 1.8rem;
542
  color: var(--secondary-text-color-light);
543
  cursor: pointer;
544
  transition: color 0.3s, transform 0.2s;
@@ -555,15 +565,15 @@ body.dark-mode .close:hover {
555
  }
556
 
557
  .modal h2 {
558
- font-size: 1.6rem;
559
  font-weight: 700;
560
- margin-bottom: 20px;
561
  text-align: center;
562
  }
563
  .cart-item {
564
  display: flex;
565
  align-items: center;
566
- padding: 15px 0;
567
  border-bottom: 1px solid var(--border-color-light);
568
  }
569
  body.dark-mode .cart-item {
@@ -573,247 +583,239 @@ body.dark-mode .cart-item {
573
  border-bottom: none;
574
  }
575
  .cart-item img {
576
- width: 60px;
577
- height: 60px;
578
  object-fit: contain;
579
- border-radius: 10px;
580
- margin-right: 15px;
581
- background-color: #f0f0f0;
582
  }
583
  body.dark-mode .cart-item img {
584
- background-color: #3a4250;
585
  }
586
  .cart-item-details {
587
  flex-grow: 1;
588
  }
589
  .cart-item-details strong {
590
- font-size: 1.1rem;
591
  font-weight: 600;
592
  }
593
  .cart-item-details p {
594
- font-size: 0.9rem;
595
  color: var(--secondary-text-color-light);
596
  }
597
  body.dark-mode .cart-item-details p {
598
  color: var(--secondary-text-color-dark);
599
  }
600
  .cart-item-total {
601
- font-size: 1rem;
602
  font-weight: 700;
603
  color: var(--danger-color);
604
  }
605
  .quantity-input, .color-select, .model-select {
606
  width: 100%;
607
- padding: 10px;
608
  border: 1px solid var(--border-color-light);
609
- border-radius: 8px;
610
  font-size: 1rem;
611
- margin: 8px 0;
612
  background-color: var(--card-background-light);
613
  color: var(--text-color-light);
 
614
  }
615
  body.dark-mode .quantity-input, body.dark-mode .color-select, body.dark-mode .model-select {
616
  border-color: var(--border-color-dark);
617
  background-color: var(--card-background-dark);
618
  color: var(--text-color-dark);
 
619
  }
620
  .modal-buttons {
621
- margin-top: 20px;
622
  display: flex;
623
  justify-content: flex-end;
624
- gap: 10px;
625
  }
626
  .modal-buttons .product-button {
627
  width: auto;
628
- padding: 10px 20px;
629
  }
630
  .clear-cart {
631
  background-color: var(--danger-color);
632
  }
633
  .clear-cart:hover {
634
  background-color: var(--danger-dark-color);
635
- box-shadow: 0 4px 15px rgba(239, 68, 68, 0.4);
636
  }
637
  .order-button {
638
  background-color: var(--accent-color);
639
  }
640
  .order-button:hover {
641
  background-color: var(--accent-dark-color);
642
- box-shadow: 0 4px 15px rgba(16, 185, 129, 0.4);
643
  }
644
 
645
  .swiper-container {
646
- max-width: 400px;
647
- margin: 0 auto 20px;
648
- border-radius: 15px;
649
  overflow: hidden;
650
- box-shadow: 0 5px 20px rgba(0,0,0,0.1);
651
  }
652
  body.dark-mode .swiper-container {
653
- box-shadow: 0 5px 20px rgba(0,0,0,0.3);
654
  }
655
  .swiper-slide {
656
- background-color: #f0f0f0;
657
  display: flex;
658
  justify-content: center;
659
  align-items: center;
660
- min-height: 250px;
661
  }
662
  body.dark-mode .swiper-slide {
663
- background-color: #3a4250;
664
  }
665
  .swiper-slide img, .swiper-slide video {
666
  max-width: 100%;
667
- max-height: 300px;
668
  object-fit: contain;
669
  }
670
  .swiper-button-next, .swiper-button-prev {
671
  color: var(--primary-color) !important;
672
- background-color: rgba(255,255,255,0.8);
673
  border-radius: 50%;
674
- width: 40px;
675
- height: 40px;
676
  display: flex;
677
  align-items: center;
678
  justify-content: center;
679
  transition: background-color 0.3s;
 
680
  }
681
  .swiper-button-next:hover, .swiper-button-prev:hover {
682
- background-color: rgba(255,255,255,1);
683
  }
684
  body.dark-mode .swiper-button-next, body.dark-mode .swiper-button-prev {
685
- background-color: rgba(45, 55, 72, 0.8);
686
  }
687
  body.dark-mode .swiper-button-next:hover, body.dark-mode .swiper-button-prev:hover {
688
- background-color: rgba(45, 55, 72, 1);
689
  }
690
  .swiper-pagination-bullet {
691
  background-color: var(--primary-color) !important;
692
  }
693
 
694
- .product-detail-page {
695
- background: var(--card-background-light);
696
- margin: 50px auto;
697
- padding: 30px;
698
- border-radius: 20px;
699
- width: 95%;
700
- max-width: 750px;
701
- box-shadow: 0 15px 40px rgba(0,0,0,0.3);
702
- position: relative;
703
  }
704
- body.dark-mode .product-detail-page {
705
- background: var(--card-background-dark);
706
- color: var(--text-color-dark);
707
- box-shadow: 0 15px 40px rgba(0,0,0,0.5);
708
  }
709
- .product-detail-page h1 {
710
  font-size: 2.2rem;
711
  font-weight: 700;
712
  margin-bottom: 25px;
713
  text-align: center;
714
  color: var(--text-color-light);
715
  }
716
- body.dark-mode .product-detail-page h1 {
717
  color: var(--text-color-dark);
718
  }
719
- .product-detail-page p {
720
  margin-bottom: 10px;
721
  font-size: 1rem;
722
  }
723
- .product-detail-page p strong {
724
  color: var(--text-color-light);
725
  }
726
- body.dark-mode .product-detail-page p strong {
727
  color: var(--text-color-dark);
728
  }
729
- .product-detail-page .price {
730
  font-size: 1.8rem;
731
  color: var(--danger-color);
732
  font-weight: 700;
733
  margin-bottom: 20px;
734
  text-align: center;
735
  }
736
- .product-detail-page .description {
737
  margin-bottom: 20px;
738
  white-space: pre-wrap;
739
  }
740
- .product-detail-actions {
741
  display: flex;
742
  justify-content: center;
743
  gap: 15px;
744
  margin-top: 30px;
745
  }
746
- .product-detail-actions .product-button {
747
  width: auto;
748
  padding: 12px 25px;
749
  }
750
- .back-to-catalog {
751
- margin-bottom: 20px;
752
- text-align: left;
753
- }
754
-
755
 
756
  @media (max-width: 768px) {
 
 
 
757
  .header h1 {
758
- font-size: 1.4rem;
759
  }
760
  .category-filter {
761
  font-size: 0.85rem;
762
  padding: 8px 15px;
763
  }
764
  .products-grid {
765
- grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
766
- gap: 15px;
767
  }
768
  .product h2 {
769
- font-size: 0.95rem;
770
  }
771
  .product-price {
772
- font-size: 1.05rem;
773
  }
774
  .product-description {
775
- font-size: 0.75rem;
776
  }
777
  .product-button {
778
- font-size: 0.85rem;
779
- padding: 8px;
780
  }
781
  #cart-button {
782
- width: 45px;
783
- height: 45px;
784
- font-size: 1.1rem;
785
- bottom: 15px;
786
- right: 15px;
787
  }
788
  .modal-content {
789
  margin: 20px auto;
790
- padding: 20px;
791
- max-height: calc(100vh - 40px);
792
  }
793
  .close {
794
- font-size: 1.5rem;
795
- top: 10px;
796
- right: 15px;
797
  }
798
  .modal h2 {
799
- font-size: 1.4rem;
800
  }
801
  .cart-item img {
802
- width: 45px;
803
- height: 45px;
804
- }
805
- .product-detail-page {
806
- margin: 20px auto;
807
- padding: 20px;
808
- max-height: calc(100vh - 40px);
809
  }
810
- .product-detail-page h1 {
811
- font-size: 1.8rem;
812
  }
813
- .product-detail-page .price {
814
- font-size: 1.5rem;
815
  }
816
- .product-detail-actions {
817
  flex-direction: column;
818
  gap: 10px;
819
  }
@@ -826,13 +828,15 @@ def catalog():
826
  products = data.get('products', [])
827
  categories = data.get('categories', [])
828
 
 
 
829
  return render_template_string('''
830
  <!DOCTYPE html>
831
  <html lang="ru">
832
  <head>
833
  <meta charset="UTF-8">
834
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
835
- <title>ZZIRIX - сотовые аксессуары оптом </title>
836
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
837
  <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
838
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.css">
@@ -871,7 +875,7 @@ def catalog():
871
  data-name="{{ product['name']|lower }}"
872
  data-description="{{ product['description']|lower }}"
873
  data-category="{{ product.get('category', 'Без категории')|lower }}">
874
- <a href="{{ url_for('product_page', product_id=product.id) }}" class="product-image">
875
  {% if product.get('photos') and product['photos']|length > 0 %}
876
  {% set filename = product['photos'][0] %}
877
  {% if filename.endswith(('.mp4', '.mov', '.webm')) %}
@@ -891,7 +895,7 @@ def catalog():
891
  <div class="product-price">{{ "%.2f"|format(product['price']) }} с</div>
892
  <p class="product-description">{{ product['description'][:50] }}{% if product['description']|length > 50 %}...{% endif %}</p>
893
  <div class="product-actions">
894
- <a href="{{ url_for('product_page', product_id=product.id) }}" class="product-button">Подробнее</a>
895
  <button class="product-button add-to-cart" onclick="openQuantityModal('{{ product.id }}')">В корзину</button>
896
  </div>
897
  </div>
@@ -929,6 +933,30 @@ def catalog():
929
  </div>
930
  </div>
931
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
932
  <button id="cart-button" onclick="openCartModal()">
933
  <i class="fas fa-shopping-cart"></i>
934
  <span id="cart-count" class="cart-count">0</span>
@@ -938,6 +966,7 @@ def catalog():
938
  <script>
939
  const products = {{ products|tojson }};
940
  let selectedProductId = null;
 
941
 
942
  function toggleTheme() {
943
  document.body.classList.toggle('dark-mode');
@@ -958,6 +987,10 @@ def catalog():
958
 
959
  function closeModal(modalId) {
960
  document.getElementById(modalId).style.display = "none";
 
 
 
 
961
  }
962
 
963
  function openQuantityModal(productId) {
@@ -1020,7 +1053,6 @@ def catalog():
1020
  let cart = JSON.parse(localStorage.getItem('cart') || '[]');
1021
  const product = products.find(p => p.id === selectedProductId);
1022
 
1023
- // Generate a unique ID for the cart item based on product ID, color, and model
1024
  const cartItemId = `${product.id}-${color}-${model}`;
1025
  const existingItem = cart.find(item => item.cartId === cartItemId);
1026
 
@@ -1028,8 +1060,8 @@ def catalog():
1028
  existingItem.quantity += quantity;
1029
  } else {
1030
  cart.push({
1031
- cartId: cartItemId, // Unique ID for this specific combination in cart
1032
- productId: product.id, // Original product ID
1033
  name: product.name,
1034
  price: product.price,
1035
  photo: product.photos && product.photos.length > 0 ? product.photos[0] : '',
@@ -1066,9 +1098,9 @@ def catalog():
1066
  <img src="${photoSrc}" alt="${item.name}">
1067
  <div class="cart-item-details">
1068
  <strong>${item.name}</strong>
1069
- <p>${item.price} с × ${item.quantity} (Цвет: ${item.color}, Модель: ${item.model})</p>
1070
  </div>
1071
- <span class="cart-item-total">${itemTotal} с</span>
1072
  </div>
1073
  `;
1074
  }).join('');
@@ -1092,7 +1124,7 @@ def catalog():
1092
  });
1093
  orderText += `*Итого к оплате: ${total.toFixed(2)} с*%0A%0AЖду подтверждения заказа.`;
1094
  window.open(`https://api.whatsapp.com/send?phone=996705665777&text=${orderText}`, '_blank');
1095
- clearCart(); // Optionally clear cart after ordering
1096
  }
1097
 
1098
  function clearCart() {
@@ -1104,6 +1136,10 @@ def catalog():
1104
  window.onclick = function(event) {
1105
  if (event.target.classList.contains('modal')) {
1106
  event.target.style.display = "none";
 
 
 
 
1107
  }
1108
  }
1109
 
@@ -1131,6 +1167,80 @@ def catalog():
1131
  });
1132
  }
1133
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1134
  updateCartButton();
1135
  </script>
1136
  </body>
@@ -1149,6 +1259,7 @@ def catalog_by_category(category_name):
1149
  categories = data.get('categories', [])
1150
 
1151
  products = [p for p in products_all if p.get('category', '').lower() == category_name.lower()]
 
1152
 
1153
  return render_template_string('''
1154
  <!DOCTYPE html>
@@ -1183,7 +1294,7 @@ def catalog_by_category(category_name):
1183
  <div class="filters-container">
1184
  <button class="category-filter" data-category="all" onclick="window.location.href='{{ url_for('catalog') }}'">Все категории</button>
1185
  {% for cat in categories %}
1186
- <button class="category-filter {% if cat | lower == category_name | lower %}active{% endif %}" data-category="{{ cat | lower }}">{{ cat }}</button>
1187
  {% endfor %}
1188
  </div>
1189
  <div class="search-container">
@@ -1195,7 +1306,7 @@ def catalog_by_category(category_name):
1195
  data-name="{{ product['name']|lower }}"
1196
  data-description="{{ product['description']|lower }}"
1197
  data-category="{{ product.get('category', 'Без категории')|lower }}">
1198
- <a href="{{ url_for('product_page', product_id=product.id) }}" class="product-image">
1199
  {% if product.get('photos') and product['photos']|length > 0 %}
1200
  {% set filename = product['photos'][0] %}
1201
  {% if filename.endswith(('.mp4', '.mov', '.webm')) %}
@@ -1215,7 +1326,7 @@ def catalog_by_category(category_name):
1215
  <div class="product-price">{{ "%.2f"|format(product['price']) }} с</div>
1216
  <p class="product-description">{{ product['description'][:50] }}{% if product['description']|length > 50 %}...{% endif %}</p>
1217
  <div class="product-actions">
1218
- <a href="{{ url_for('product_page', product_id=product.id) }}" class="product-button">Подробнее</a>
1219
  <button class="product-button add-to-cart" onclick="openQuantityModal('{{ product.id }}')">В корзину</button>
1220
  </div>
1221
  </div>
@@ -1253,6 +1364,30 @@ def catalog_by_category(category_name):
1253
  </div>
1254
  </div>
1255
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1256
  <button id="cart-button" onclick="openCartModal()">
1257
  <i class="fas fa-shopping-cart"></i>
1258
  <span id="cart-count" class="cart-count">0</span>
@@ -1260,8 +1395,9 @@ def catalog_by_category(category_name):
1260
 
1261
  <script src="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.js"></script>
1262
  <script>
1263
- const products = {{ products_all|tojson }}; // Use all products for client-side lookup
1264
  let selectedProductId = null;
 
1265
 
1266
  function toggleTheme() {
1267
  document.body.classList.toggle('dark-mode');
@@ -1282,6 +1418,10 @@ def catalog_by_category(category_name):
1282
 
1283
  function closeModal(modalId) {
1284
  document.getElementById(modalId).style.display = "none";
 
 
 
 
1285
  }
1286
 
1287
  function openQuantityModal(productId) {
@@ -1344,7 +1484,6 @@ def catalog_by_category(category_name):
1344
  let cart = JSON.parse(localStorage.getItem('cart') || '[]');
1345
  const product = products.find(p => p.id === selectedProductId);
1346
 
1347
- // Generate a unique ID for the cart item based on product ID, color, and model
1348
  const cartItemId = `${product.id}-${color}-${model}`;
1349
  const existingItem = cart.find(item => item.cartId === cartItemId);
1350
 
@@ -1352,8 +1491,8 @@ def catalog_by_category(category_name):
1352
  existingItem.quantity += quantity;
1353
  } else {
1354
  cart.push({
1355
- cartId: cartItemId, // Unique ID for this specific combination in cart
1356
- productId: product.id, // Original product ID
1357
  name: product.name,
1358
  price: product.price,
1359
  photo: product.photos && product.photos.length > 0 ? product.photos[0] : '',
@@ -1416,7 +1555,7 @@ def catalog_by_category(category_name):
1416
  });
1417
  orderText += `*Итого к оплате: ${total.toFixed(2)} с*%0A%0AЖду подтверждения заказа.`;
1418
  window.open(`https://api.whatsapp.com/send?phone=996705665777&text=${orderText}`, '_blank');
1419
- clearCart(); // Optionally clear cart after ordering
1420
  }
1421
 
1422
  function clearCart() {
@@ -1428,355 +1567,92 @@ def catalog_by_category(category_name):
1428
  window.onclick = function(event) {
1429
  if (event.target.classList.contains('modal')) {
1430
  event.target.style.display = "none";
 
 
 
 
1431
  }
1432
  }
1433
 
1434
  document.getElementById('search-input').addEventListener('input', filterProducts);
1435
- document.querySelectorAll('.filters-container .category-filter').forEach(filter => {
1436
- filter.addEventListener('click', function() {
1437
- // For category filter on catalog_by_category page, redirect to correct URL
1438
- if (this.dataset.category === 'all') {
1439
- window.location.href = "{{ url_for('catalog') }}";
1440
- } else {
1441
- window.location.href = "{{ url_for('catalog_by_category', category_name='') }}" + this.dataset.category;
1442
- }
1443
- });
1444
- });
1445
 
1446
  function filterProducts() {
1447
  const searchTerm = document.getElementById('search-input').value.toLowerCase().trim();
1448
- const activeCategory = "{{ category_name | lower }}"; // Use the current category name from Flask
1449
 
1450
  document.querySelectorAll('.product').forEach(product => {
1451
  const name = product.getAttribute('data-name');
1452
  const description = product.getAttribute('data-description');
1453
- const category = product.getAttribute('data-category'); // Already filtered by Flask for this page
1454
 
1455
  const matchesSearch = (name && name.includes(searchTerm)) || (description && description.includes(searchTerm));
1456
 
1457
- // Only filter by search term, category is already handled by the route
1458
  product.style.display = matchesSearch ? 'flex' : 'none';
1459
  });
1460
  }
1461
 
1462
- updateCartButton();
1463
- </script>
1464
- </body>
1465
- </html>
1466
- ''',
1467
- products=products,
1468
- categories=categories,
1469
- category_name=category_name,
1470
- products_all=products_all, # Pass all products for JS lookup
1471
- repo_id=REPO_ID,
1472
- LOGO_URL=LOGO_URL
1473
- )
1474
-
1475
- @app.route('/product/<product_id>')
1476
- def product_page(product_id):
1477
- data = load_data()
1478
- product = next((p for p in data.get('products', []) if p.get('id') == product_id), None)
1479
- categories = data.get('categories', [])
1480
-
1481
- if product is None:
1482
- flash('Товар не найден.', 'error')
1483
- return redirect(url_for('catalog'))
1484
-
1485
- return render_template_string('''
1486
- <!DOCTYPE html>
1487
- <html lang="ru">
1488
- <head>
1489
- <meta charset="UTF-8">
1490
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
1491
- <title>{{ product.name }} - ZZIRIX</title>
1492
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
1493
- <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
1494
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.css">
1495
- <style>''' + BASE_STYLE + '''</style>
1496
- </head>
1497
- <body>
1498
- <div class="container">
1499
- <div class="header">
1500
- <div class="header-info">
1501
- <img src="''' + LOGO_URL + '''" alt="Logo" class="header-logo">
1502
- <h1>Каталог ZZIRIX</h1>
1503
- </div>
1504
- <button class="theme-toggle" onclick="toggleTheme()">
1505
- <i class="fas fa-moon"></i>
1506
- </button>
1507
- </div>
1508
- {% with messages = get_flashed_messages(with_categories=true) %}
1509
- {% if messages %}
1510
- {% for category, message in messages %}
1511
- <div class="flash {{ category }}">{{ message }}</div>
1512
- {% endfor %}
1513
- {% endif %}
1514
- {% endwith %}
1515
- <div class="back-to-catalog">
1516
- <a href="{{ url_for('catalog_by_category', category_name=product.category | lower) }}" class="product-button" style="width: auto; padding: 8px 15px; font-size: 0.9rem;">&larr; Назад в категорию</a>
1517
- </div>
1518
- <div class="product-detail-page">
1519
- <h1>{{ product['name'] }}</h1>
1520
- <div class="swiper-container" style="max-width: 450px; margin: 0 auto 30px;">
1521
- <div class="swiper-wrapper">
1522
- {% if product.get('photos') %}
1523
- {% for photo in product['photos'] %}
1524
- <div class="swiper-slide swiper-zoom-container" style="background-color: #f0f0f0; display: flex; justify-content: center; align-items: center; border-radius: 10px;">
1525
- {% if photo.endswith(('.mp4', '.mov', '.webm')) %}
1526
- <video controls preload="metadata" style="max-width: 100%; max-height: 300px; object-fit: contain;"><source src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo }}" type="video/mp4"></video>
1527
- {% else %}
1528
- <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo }}"
1529
- alt="{{ product['name'] }}"
1530
- style="max-width: 100%; max-height: 300px; object-fit: contain;">
1531
- {% endif %}
1532
- </div>
1533
- {% endfor %}
1534
- {% else %}
1535
- <div class="swiper-slide swiper-zoom-container" style="background-color: #f0f0f0; display: flex; justify-content: center; align-items: center; border-radius: 10px;">
1536
- <img src="https://via.placeholder.com/300x300?text=No+Image" alt="No Image">
1537
- </div>
1538
- {% endif %}
1539
- </div>
1540
- <div class="swiper-pagination"></div>
1541
- <div class="swiper-button-next"></div>
1542
- <div class="swiper-button-prev"></div>
1543
- </div>
1544
- <p><strong>Категория:</strong> <span style="color: var(--primary-color);">{{ product.get('category', 'Без категории') }}</span></p>
1545
- <p class="price">{{ "%.2f"|format(product['price']) }} с</p>
1546
- <p class="description"><strong>Описание:</strong> {{ product['description'] }}</p>
1547
- <p><strong>Доступные цвета:</strong> <span style="color: var(--secondary-text-color-light);">{{ product.get('colors', ['Нет цветов'])|join(', ') }}</span></p>
1548
- <p><strong>Доступные модели:</strong> <span style="color: var(--secondary-text-color-light);">{{ product.get('models', ['Нет моделей'])|join(', ') }}</span></p>
1549
- <div class="product-detail-actions">
1550
- <button class="product-button add-to-cart" onclick="openQuantityModal('{{ product.id }}')">В корзину</button>
1551
- </div>
1552
- </div>
1553
- </div>
1554
-
1555
- <div id="quantityModal" class="modal">
1556
- <div class="modal-content">
1557
- <span class="close" onclick="closeModal('quantityModal')">×</span>
1558
- <h2>Укажите количество, цвет и модель</h2>
1559
- <input type="number" id="quantityInput" class="quantity-input" min="1" value="1">
1560
- <select id="colorSelect" class="color-select"></select>
1561
- <select id="modelSelect" class="model-select"></select>
1562
- <div class="modal-buttons">
1563
- <button class="product-button order-button" onclick="confirmAddToCart()">Добавить</button>
1564
- </div>
1565
- </div>
1566
- </div>
1567
-
1568
- <div id="cartModal" class="modal">
1569
- <div class="modal-content">
1570
- <span class="close" onclick="closeModal('cartModal')">×</span>
1571
- <h2>Корзина</h2>
1572
- <div id="cartContent"></div>
1573
- <div style="margin-top: 20px; text-align: right;">
1574
- <strong style="font-size: 1.2rem;">Итого: <span id="cartTotal">0</span> с</strong>
1575
- <div class="modal-buttons">
1576
- <button class="product-button clear-cart" onclick="clearCart()">Очистить</button>
1577
- <button class="product-button order-button" onclick="orderViaWhatsApp()">Заказать</button>
1578
- </div>
1579
- </div>
1580
- </div>
1581
- </div>
1582
-
1583
- <button id="cart-button" onclick="openCartModal()">
1584
- <i class="fas fa-shopping-cart"></i>
1585
- <span id="cart-count" class="cart-count">0</span>
1586
- </button>
1587
-
1588
- <script src="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.js"></script>
1589
- <script>
1590
- const products = {{ data.products|tojson }};
1591
- let selectedProductId = null;
1592
-
1593
- function toggleTheme() {
1594
- document.body.classList.toggle('dark-mode');
1595
- const icon = document.querySelector('.theme-toggle i');
1596
- icon.classList.toggle('fa-moon');
1597
- icon.classList.toggle('fa-sun');
1598
- localStorage.setItem('theme', document.body.classList.contains('dark-mode') ? 'dark' : 'light');
1599
- }
1600
-
1601
- if (localStorage.getItem('theme') === 'dark') {
1602
- document.body.classList.add('dark-mode');
1603
- const icon = document.querySelector('.theme-toggle i');
1604
- if (icon) icon.classList.replace('fa-moon', 'fa-sun');
1605
- } else {
1606
- const icon = document.querySelector('.theme-toggle i');
1607
- if (icon) icon.classList.replace('fa-sun', 'fa-moon');
1608
- }
1609
-
1610
- function closeModal(modalId) {
1611
- document.getElementById(modalId).style.display = "none";
1612
- }
1613
-
1614
- function openQuantityModal(productId) {
1615
  selectedProductId = productId;
1616
  const product = products.find(p => p.id === productId);
1617
  if (!product) {
1618
  alert('Ошибка: Товар не найден.');
1619
  return;
1620
  }
1621
-
1622
- const colorSelect = document.getElementById('colorSelect');
1623
- colorSelect.innerHTML = '';
1624
- if (product.colors && product.colors.length > 0) {
1625
- product.colors.forEach(color => {
1626
- const option = document.createElement('option');
1627
- option.value = color;
1628
- option.text = color;
1629
- colorSelect.appendChild(option);
1630
- });
1631
- } else {
1632
- const option = document.createElement('option');
1633
- option.value = 'Не указан';
1634
- option.text = 'Цвет не указан';
1635
- colorSelect.appendChild(option);
1636
- }
1637
 
1638
- const modelSelect = document.getElementById('modelSelect');
1639
- modelSelect.innerHTML = '';
1640
- if (product.models && product.models.length > 0) {
1641
- product.models.forEach(model => {
1642
- const option = document.createElement('option');
1643
- option.value = model;
1644
- option.text = model;
1645
- modelSelect.appendChild(option);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1646
  });
1647
  } else {
1648
- const option = document.createElement('option');
1649
- option.value = 'Не указана';
1650
- option.text = 'Модель не указана';
1651
- modelSelect.appendChild(option);
 
1652
  }
1653
-
1654
- document.getElementById('quantityModal').style.display = 'block';
1655
- document.getElementById('quantityInput').value = 1;
1656
- }
1657
-
1658
- function confirmAddToCart() {
1659
- if (selectedProductId === null) return;
1660
- const quantityInput = document.getElementById('quantityInput');
1661
- const quantity = parseInt(quantityInput.value) || 1;
1662
- const color = document.getElementById('colorSelect').value;
1663
- const model = document.getElementById('modelSelect').value;
1664
 
1665
- if (quantity <= 0) {
1666
- alert("Укажите количество больше 0.");
1667
- quantityInput.focus();
1668
- return;
1669
- }
1670
 
1671
- let cart = JSON.parse(localStorage.getItem('cart') || '[]');
1672
- const product = products.find(p => p.id === selectedProductId);
1673
-
1674
- // Generate a unique ID for the cart item based on product ID, color, and model
1675
- const cartItemId = `${product.id}-${color}-${model}`;
1676
- const existingItem = cart.find(item => item.cartId === cartItemId);
1677
-
1678
- if (existingItem) {
1679
- existingItem.quantity += quantity;
1680
- } else {
1681
- cart.push({
1682
- cartId: cartItemId, // Unique ID for this specific combination in cart
1683
- productId: product.id, // Original product ID
1684
- name: product.name,
1685
- price: product.price,
1686
- photo: product.photos && product.photos.length > 0 ? product.photos[0] : '',
1687
- quantity: quantity,
1688
- color: color,
1689
- model: model
1690
- });
1691
  }
1692
-
1693
- localStorage.setItem('cart', JSON.stringify(cart));
1694
- closeModal('quantityModal');
1695
- updateCartButton();
1696
- alert(`"${product.name}" добавлен в корзину.`);
1697
- }
1698
-
1699
- function updateCartButton() {
1700
- const cart = JSON.parse(localStorage.getItem('cart') || '[]');
1701
- const cartCount = cart.reduce((sum, item) => sum + item.quantity, 0);
1702
- document.getElementById('cart-count').textContent = cartCount;
1703
- document.getElementById('cart-button').style.display = cartCount > 0 ? 'flex' : 'none';
1704
- }
1705
-
1706
- function openCartModal() {
1707
- const cart = JSON.parse(localStorage.getItem('cart') || '[]');
1708
- const cartContent = document.getElementById('cartContent');
1709
- let total = 0;
1710
-
1711
- cartContent.innerHTML = cart.length === 0 ? '<p style="text-align: center; color: var(--secondary-text-color-light);">Корзина пуста</p>' : cart.map((item, index) => {
1712
- const itemTotal = item.price * item.quantity;
1713
- total += itemTotal;
1714
- const photoSrc = item.photo ? `https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/${item.photo}` : 'https://via.placeholder.com/60x60?text=No+Image';
1715
- return `
1716
- <div class="cart-item">
1717
- <img src="${photoSrc}" alt="${item.name}">
1718
- <div class="cart-item-details">
1719
- <strong>${item.name}</strong>
1720
- <p>${item.price.toFixed(2)} с × ${item.quantity} (Цвет: ${item.color}, Модель: ${item.model})</p>
1721
- </div>
1722
- <span class="cart-item-total">${itemTotal.toFixed(2)} с</span>
1723
- </div>
1724
- `;
1725
- }).join('');
1726
-
1727
- document.getElementById('cartTotal').textContent = total.toFixed(2);
1728
- document.getElementById('cartModal').style.display = 'block';
1729
- }
1730
-
1731
- function orderViaWhatsApp() {
1732
- const cart = JSON.parse(localStorage.getItem('cart') || '[]');
1733
- if (cart.length === 0) {
1734
- alert("Корзина пуста!");
1735
- return;
1736
- }
1737
- let total = 0;
1738
- let orderText = "Здравствуйте, меня интересует заказ:%0A%0A";
1739
- cart.forEach((item, index) => {
1740
- const itemTotal = item.price * item.quantity;
1741
- total += itemTotal;
1742
- orderText += `${index + 1}. ${item.name}%0AКоличество: ${item.quantity}%0AЦена: ${item.price.toFixed(2)} с%0AЦвет: ${item.color}%0AМодель: ${item.model}%0A--%0A`;
1743
- });
1744
- orderText += `*Итого к оплате: ${total.toFixed(2)} с*%0A%0AЖду подтверждения заказа.`;
1745
- window.open(`https://api.whatsapp.com/send?phone=996705665777&text=${orderText}`, '_blank');
1746
- clearCart(); // Optionally clear cart after ordering
1747
- }
1748
-
1749
- function clearCart() {
1750
- localStorage.removeItem('cart');
1751
- openCartModal();
1752
- updateCartButton();
1753
- }
1754
-
1755
- window.onclick = function(event) {
1756
- if (event.target.classList.contains('modal')) {
1757
- event.target.style.display = "none";
1758
- }
1759
- }
1760
-
1761
- function initializeSwiper() {
1762
- const swiper = new Swiper('.swiper-container', {
1763
  slidesPerView: 1,
1764
  spaceBetween: 20,
1765
  loop: true,
1766
  grabCursor: true,
1767
- pagination: { el: '.swiper-pagination', clickable: true },
1768
- navigation: { nextEl: '.swiper-button-next', prevEl: '.swiper-button-prev' },
1769
  zoom: { maxRatio: 3 },
1770
  on: {
1771
  init: function () {
1772
  this.slides.forEach(slide => {
1773
  const img = slide.querySelector('img');
1774
  if (img && !img.complete) {
1775
- img.onload = () => swiper.update();
1776
  }
1777
  const video = slide.querySelector('video');
1778
  if (video) {
1779
- video.onloadedmetadata = () => swiper.update();
1780
  }
1781
  });
1782
  },
@@ -1785,18 +1661,20 @@ def product_page(product_id):
1785
  }
1786
  }
1787
  });
 
1788
  }
1789
- initializeSwiper();
1790
  updateCartButton();
1791
  </script>
1792
  </body>
1793
  </html>
1794
  ''',
1795
- product=product,
1796
  categories=categories,
 
 
1797
  repo_id=REPO_ID,
1798
- LOGO_URL=LOGO_URL,
1799
- data=data # Pass full data for JS to get all products
1800
  )
1801
 
1802
  @app.route('/admin', methods=['GET', 'POST'])
@@ -1933,7 +1811,6 @@ def admin():
1933
  uploads_dir = 'uploads'
1934
  os.makedirs(uploads_dir, exist_ok=True)
1935
 
1936
- # Delete old photos from HF if they exist
1937
  for old_filename in product_to_edit.get('photos', []):
1938
  try:
1939
  api.delete_file(path_in_repo=f"photos/{old_filename}", repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE)
@@ -1975,7 +1852,6 @@ def admin():
1975
  product_to_delete = next((p for p in products if p.get('id') == product_id), None)
1976
 
1977
  if product_to_delete:
1978
- # Delete associated photos from HF
1979
  for filename in product_to_delete.get('photos', []):
1980
  try:
1981
  api.delete_file(path_in_repo=f"photos/{filename}", repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE)
@@ -1990,6 +1866,8 @@ def admin():
1990
  flash('Ошибка: Товар не найден для удаления.', 'error')
1991
  return redirect(url_for('admin'))
1992
 
 
 
1993
  return render_template_string('''
1994
  <!DOCTYPE html>
1995
  <html lang="ru">
@@ -2042,7 +1920,7 @@ def admin():
2042
  form {
2043
  background: var(--card-background-light);
2044
  padding: 30px;
2045
- border-radius: 20px;
2046
  box-shadow: var(--shadow-light);
2047
  margin-bottom: 30px;
2048
  transition: background 0.3s, box-shadow 0.3s;
@@ -2066,7 +1944,7 @@ def admin():
2066
  padding: 12px;
2067
  margin-bottom: 10px;
2068
  border: 1px solid var(--border-color-light);
2069
- border-radius: 10px;
2070
  font-size: 1rem;
2071
  transition: all 0.3s ease;
2072
  background-color: var(--card-background-light);
@@ -2085,7 +1963,7 @@ def admin():
2085
  button {
2086
  padding: 12px 22px;
2087
  border: none;
2088
- border-radius: 10px;
2089
  background-color: var(--primary-color);
2090
  color: white;
2091
  font-weight: 500;
@@ -2114,7 +1992,7 @@ def admin():
2114
  .product-item, .category-item {
2115
  background: var(--card-background-light);
2116
  padding: 20px;
2117
- border-radius: 18px;
2118
  box-shadow: var(--shadow-light);
2119
  transition: background 0.3s, box-shadow 0.3s;
2120
  }
@@ -2143,7 +2021,7 @@ def admin():
2143
  margin-top: 20px;
2144
  padding: 20px;
2145
  background: var(--background-light);
2146
- border-radius: 15px;
2147
  box-shadow: inset 0 2px 5px rgba(0,0,0,0.05);
2148
  }
2149
  body.dark-mode .edit-form {
@@ -2164,7 +2042,7 @@ def admin():
2164
  background-color: #ccc;
2165
  color: #555;
2166
  padding: 8px 12px;
2167
- border-radius: 8px;
2168
  font-size: 0.8rem;
2169
  margin-top: 0;
2170
  flex-shrink: 0;
@@ -2191,14 +2069,14 @@ def admin():
2191
  margin-top: 10px;
2192
  margin-bottom: 15px;
2193
  }
2194
- .product-photos-preview img {
2195
  max-width: 90px;
2196
  height: auto;
2197
- border-radius: 8px;
2198
  border: 1px solid var(--border-color-light);
2199
  box-shadow: 0 2px 8px rgba(0,0,0,0.05);
2200
  }
2201
- body.dark-mode .product-photos-preview img {
2202
  border-color: var(--border-color-dark);
2203
  box-shadow: 0 2px 8px rgba(0,0,0,0.15);
2204
  }
@@ -2225,6 +2103,12 @@ def admin():
2225
  width: 100%;
2226
  max-width: 400px;
2227
  margin-left: 0;
 
 
 
 
 
 
2228
  }
2229
  </style>
2230
  </head>
@@ -2340,7 +2224,7 @@ def admin():
2340
  alt="{{ product['name'] }}"
2341
  loading="lazy"
2342
  controls
2343
- style="max-width: 90px; height: auto; border-radius: 8px; border: 1px solid var(--border-color-light); box-shadow: 0 2px 8px rgba(0,0,0,0.05);">
2344
  </video>
2345
  {% else %}
2346
  <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo }}?r={{ random.randint(1,1000) }}"
@@ -2452,7 +2336,7 @@ def admin():
2452
  @app.route('/backup', methods=['POST'])
2453
  def backup():
2454
  try:
2455
- data = load_data() # Ensure latest data is loaded before saving/uploading
2456
  save_data(data)
2457
  flash('Резервная копия успешно создана и загружена.', 'success')
2458
  return redirect(url_for('admin'))
@@ -2464,7 +2348,7 @@ def backup():
2464
  @app.route('/download', methods=['GET'])
2465
  def download():
2466
  try:
2467
- download_db_from_hf() # Ensure the latest version is downloaded before sending
2468
  if os.path.exists(DATA_FILE):
2469
  return send_file(DATA_FILE, as_attachment=True, mimetype='application/json', download_name='data_zzirix_backup.json')
2470
  flash('Файл базы данных не найден после попытки скачивания.', 'error')
@@ -2481,7 +2365,7 @@ if __name__ == '__main__':
2481
  logging.info("Начальная проверка и обновление структуры данных...")
2482
  try:
2483
  current_data = load_data()
2484
- save_data(current_data) # This will save the initialized structure and upload to HF
2485
  logging.info("Структура данных проверена и при необходимости обновлена.")
2486
  except Exception as e:
2487
  logging.error(f"Ошибка во время начальной проверки/обновления структуры данных: {e}")
@@ -2489,4 +2373,4 @@ if __name__ == '__main__':
2489
  backup_thread = threading.Thread(target=periodic_backup, daemon=True)
2490
  backup_thread.start()
2491
  logging.info("Запуск Flask приложения...")
2492
- app.run(debug=True, host='0.0.0.0', port=7860)
 
 
1
  from flask import Flask, render_template_string, request, redirect, url_for, flash, send_file
2
  import json
3
  import os
 
16
  app.secret_key = os.getenv("FLASK_SECRET_KEY", "zzirix_secret_key_for_cart")
17
  DATA_FILE = 'data_zzirix.json'
18
 
19
+ REPO_ID = "Kgshop/clients"
20
  HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
21
  HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") or HF_TOKEN_WRITE
22
 
 
32
  data.setdefault('products', [])
33
  data.setdefault('orders', {})
34
 
 
35
  if any(p.get('category') == 'Без категории' for p in data['products']) and 'Без категории' not in data['categories']:
36
  data['categories'].append('Без категории')
37
 
 
45
  product.setdefault('price', 0.0)
46
  product.setdefault('colors', [])
47
  product.setdefault('models', [])
48
+ product.setdefault('photos', [])
49
+ product.setdefault('in_stock', True)
50
+ product.setdefault('is_top', False)
51
 
 
 
52
  if not product['photos'] and 'media' in product and product['media']:
53
  product['photos'] = [m['filename'] for m in product['media'] if m['type'] == 'photo']
54
 
 
55
  if 'media' in product:
56
  del product['media']
57
 
 
58
  data['categories'] = sorted(list(set(data['categories'])), key=lambda x: (x != 'Без категории', x))
59
 
60
  return data
 
169
  --accent-dark-color: #059669;
170
  --danger-color: #ef4444;
171
  --danger-dark-color: #dc2626;
172
+ --background-light: linear-gradient(135deg, #f0f2f5, #e0e5ec);
173
+ --background-dark: linear-gradient(135deg, #1f2937, #374151);
174
  --card-background-light: #ffffff;
175
  --card-background-dark: #2d3748;
176
  --text-color-light: #2d3748;
 
179
  --secondary-text-color-dark: #a0aec0;
180
  --border-color-light: #e2e8f0;
181
  --border-color-dark: #4a5568;
182
+ --shadow-light: 0 8px 25px rgba(0, 0, 0, 0.1);
183
+ --shadow-hover-light: 0 12px 35px rgba(0, 0, 0, 0.18);
184
+ --shadow-dark: 0 8px 25px rgba(0, 0, 0, 0.35);
185
+ --shadow-hover-dark: 0 12px 35px rgba(0, 0, 0, 0.5);
186
+ --border-radius-large: 22px;
187
+ --border-radius-medium: 12px;
188
+ --border-radius-small: 8px;
189
  }
190
  * {
191
  margin: 0;
 
207
  color: var(--text-color-dark);
208
  }
209
  .container {
210
+ max-width: 1400px;
211
  margin: 0 auto;
212
+ padding: 30px;
213
  flex-grow: 1;
214
  }
215
  .header {
216
  display: flex;
217
  justify-content: space-between;
218
  align-items: center;
219
+ padding: 20px 30px;
220
+ border-bottom: none;
221
+ margin-bottom: 30px;
222
+ box-shadow: 0 2px 10px rgba(0,0,0,0.05);
223
+ border-radius: var(--border-radius-large);
224
+ background: var(--card-background-light);
225
  }
226
  body.dark-mode .header {
227
  border-bottom-color: var(--border-color-dark);
228
+ box-shadow: 0 2px 10px rgba(0,0,0,0.2);
229
+ background: var(--card-background-dark);
230
  }
231
  .header-info {
232
  display: flex;
233
  align-items: center;
234
  }
235
  .header-logo {
236
+ width: 60px;
237
+ height: 60px;
238
  border-radius: 50%;
239
  object-fit: cover;
240
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15);
241
  transition: transform 0.3s ease, box-shadow 0.3s ease;
242
+ margin-right: 15px;
243
  }
244
  .header-logo:hover {
245
+ transform: scale(1.08);
246
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25);
247
  }
248
  .header h1 {
249
+ font-size: 2rem;
250
+ font-weight: 800;
251
+ color: var(--primary-color);
252
  }
253
  .theme-toggle {
254
  background: none;
255
  border: none;
256
+ font-size: 1.8rem;
257
  cursor: pointer;
258
  color: var(--secondary-text-color-light);
259
  transition: color 0.3s ease, transform 0.2s ease;
 
263
  color: var(--secondary-text-color-dark);
264
  }
265
  .theme-toggle:hover {
266
+ color: var(--accent-color);
267
+ transform: rotate(30deg);
268
  }
269
 
270
  .flash {
271
+ padding: 18px;
272
+ margin-bottom: 25px;
273
+ border-radius: var(--border-radius-medium);
274
  font-weight: 500;
275
  border: 1px solid transparent;
276
+ font-size: 1.1rem;
277
  }
278
  .flash.success {
279
  background-color: var(--accent-color);
 
287
  }
288
 
289
  .filters-container {
290
+ margin: 30px 0;
291
  display: flex;
292
  flex-wrap: wrap;
293
+ gap: 15px;
294
  justify-content: center;
295
  }
296
  .search-container {
297
+ margin: 25px 0;
298
  text-align: center;
299
  }
300
  #search-input {
301
  width: 90%;
302
  max-width: 600px;
303
+ padding: 14px 22px;
304
  font-size: 1rem;
305
+ border: none;
306
+ border-radius: var(--border-radius-medium);
307
  outline: none;
308
+ box-shadow: var(--shadow-light);
309
  transition: all 0.3s ease;
310
  background-color: var(--card-background-light);
311
  color: var(--text-color-light);
 
314
  border-color: var(--border-color-dark);
315
  background-color: var(--card-background-dark);
316
  color: var(--text-color-dark);
317
+ box-shadow: var(--shadow-dark);
318
  }
319
  #search-input:focus {
320
  border-color: var(--primary-color);
321
+ box-shadow: 0 0 10px rgba(59, 130, 246, 0.4);
322
  }
323
  .category-filter {
324
+ padding: 12px 25px;
325
+ border: none;
326
+ border-radius: var(--border-radius-medium);
327
  background-color: var(--card-background-light);
328
  cursor: pointer;
329
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
330
+ font-size: 1rem;
331
  font-weight: 500;
332
  color: var(--text-color-light);
333
  box-shadow: var(--shadow-light);
 
352
 
353
  .products-grid {
354
  display: grid;
355
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
356
+ gap: 25px;
357
+ padding: 15px;
358
  }
359
  .product {
360
  background: var(--card-background-light);
361
+ border-radius: var(--border-radius-large);
362
+ padding: 20px;
363
  box-shadow: var(--shadow-light);
364
  transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.3s ease;
365
  overflow: hidden;
 
372
  box-shadow: var(--shadow-dark);
373
  }
374
  .product:hover {
375
+ transform: translateY(-10px) scale(1.03);
376
  box-shadow: var(--shadow-hover-light);
377
  }
378
  body.dark-mode .product:hover {
 
381
  .product-image {
382
  width: 100%;
383
  aspect-ratio: 1;
384
+ background-color: #f7f7f7;
385
+ border-radius: var(--border-radius-medium);
386
  overflow: hidden;
387
  display: flex;
388
  justify-content: center;
389
  align-items: center;
390
+ margin-bottom: 15px;
391
+ cursor: pointer;
392
  }
393
  body.dark-mode .product-image {
394
+ background-color: #343e4d;
395
  }
396
  .product-image img, .product-image video {
397
  max-width: 100%;
398
  max-height: 100%;
399
  object-fit: contain;
400
  transition: transform 0.3s ease;
401
+ border-radius: var(--border-radius-medium);
402
  }
403
  .product-image img:hover, .product-image video:hover {
404
  transform: scale(1.05);
405
  }
406
  .product h2 {
407
+ font-size: 1.2rem;
408
+ font-weight: 700;
409
+ margin: 8px 0;
410
  text-align: center;
411
  white-space: nowrap;
412
  overflow: hidden;
 
417
  color: var(--text-color-dark);
418
  }
419
  .product-price {
420
+ font-size: 1.3rem;
421
  color: var(--danger-color);
422
  font-weight: 700;
423
  text-align: center;
424
+ margin: 8px 0 12px;
425
  }
426
  .product-description {
427
+ font-size: 0.9rem;
428
  color: var(--secondary-text-color-light);
429
  text-align: center;
430
+ margin-bottom: 20px;
431
  overflow: hidden;
432
  text-overflow: ellipsis;
433
  white-space: nowrap;
 
438
  .product-actions {
439
  display: flex;
440
  flex-direction: column;
441
+ gap: 10px;
442
  margin-top: auto;
443
  }
444
  .product-button {
445
  display: block;
446
  width: 100%;
447
+ padding: 12px;
448
  border: none;
449
+ border-radius: var(--border-radius-medium);
450
  background-color: var(--primary-color);
451
  color: white;
452
+ font-size: 1rem;
453
  font-weight: 500;
454
  cursor: pointer;
455
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
456
  text-align: center;
457
  text-decoration: none;
458
+ box-shadow: 0 3px 10px rgba(0,0,0,0.1);
459
  }
460
  .product-button:hover {
461
  background-color: var(--primary-dark-color);
462
+ box-shadow: 0 6px 15px rgba(59, 130, 246, 0.4);
463
+ transform: translateY(-2px);
464
  }
465
  .add-to-cart {
466
  background-color: var(--accent-color);
467
+ box-shadow: 0 3px 10px rgba(16, 185, 129, 0.2);
468
  }
469
  .add-to-cart:hover {
470
  background-color: var(--accent-dark-color);
471
+ box-shadow: 0 6px 15px rgba(16, 185, 129, 0.4);
472
  }
473
  #cart-button {
474
  position: fixed;
475
+ bottom: 30px;
476
+ right: 30px;
477
  background-color: var(--danger-color);
478
  color: white;
479
  border: none;
480
  border-radius: 50%;
481
+ width: 60px;
482
+ height: 60px;
483
+ font-size: 1.5rem;
484
  cursor: pointer;
485
+ box-shadow: 0 8px 25px rgba(239, 68, 68, 0.3);
486
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
487
  z-index: 1000;
488
  display: flex;
 
491
  }
492
  #cart-button:hover {
493
  background-color: var(--danger-dark-color);
494
+ transform: translateY(-5px) scale(1.08);
495
+ box-shadow: 0 12px 35px rgba(239, 68, 68, 0.5);
496
  }
497
  .cart-count {
498
  position: absolute;
 
501
  background-color: var(--accent-color);
502
  color: white;
503
  border-radius: 50%;
504
+ padding: 4px 8px;
505
+ font-size: 0.8rem;
506
+ min-width: 22px;
507
  text-align: center;
508
  font-weight: 600;
509
  }
 
524
  .modal-content {
525
  background: var(--card-background-light);
526
  margin: 50px auto;
527
+ padding: 40px;
528
+ border-radius: var(--border-radius-large);
529
  width: 95%;
530
  max-width: 750px;
531
+ box-shadow: var(--shadow-hover-light);
532
  animation: fadeInScale 0.3s ease-out;
533
+ max-height: 90vh;
534
  overflow-y: auto;
535
  -webkit-overflow-scrolling: touch;
536
  position: relative;
 
538
  body.dark-mode .modal-content {
539
  background: var(--card-background-dark);
540
  color: var(--text-color-dark);
541
+ box-shadow: var(--shadow-hover-dark);
542
  }
543
  @keyframes fadeInScale {
544
  from { opacity: 0; transform: translateY(-30px) scale(0.95); }
 
546
  }
547
  .close {
548
  position: absolute;
549
+ top: 20px;
550
+ right: 25px;
551
+ font-size: 2rem;
552
  color: var(--secondary-text-color-light);
553
  cursor: pointer;
554
  transition: color 0.3s, transform 0.2s;
 
565
  }
566
 
567
  .modal h2 {
568
+ font-size: 1.8rem;
569
  font-weight: 700;
570
+ margin-bottom: 25px;
571
  text-align: center;
572
  }
573
  .cart-item {
574
  display: flex;
575
  align-items: center;
576
+ padding: 18px 0;
577
  border-bottom: 1px solid var(--border-color-light);
578
  }
579
  body.dark-mode .cart-item {
 
583
  border-bottom: none;
584
  }
585
  .cart-item img {
586
+ width: 70px;
587
+ height: 70px;
588
  object-fit: contain;
589
+ border-radius: var(--border-radius-small);
590
+ margin-right: 20px;
591
+ background-color: #f7f7f7;
592
  }
593
  body.dark-mode .cart-item img {
594
+ background-color: #343e4d;
595
  }
596
  .cart-item-details {
597
  flex-grow: 1;
598
  }
599
  .cart-item-details strong {
600
+ font-size: 1.2rem;
601
  font-weight: 600;
602
  }
603
  .cart-item-details p {
604
+ font-size: 0.95rem;
605
  color: var(--secondary-text-color-light);
606
  }
607
  body.dark-mode .cart-item-details p {
608
  color: var(--secondary-text-color-dark);
609
  }
610
  .cart-item-total {
611
+ font-size: 1.1rem;
612
  font-weight: 700;
613
  color: var(--danger-color);
614
  }
615
  .quantity-input, .color-select, .model-select {
616
  width: 100%;
617
+ padding: 12px;
618
  border: 1px solid var(--border-color-light);
619
+ border-radius: var(--border-radius-medium);
620
  font-size: 1rem;
621
+ margin: 10px 0;
622
  background-color: var(--card-background-light);
623
  color: var(--text-color-light);
624
+ box-shadow: inset 0 1px 3px rgba(0,0,0,0.05);
625
  }
626
  body.dark-mode .quantity-input, body.dark-mode .color-select, body.dark-mode .model-select {
627
  border-color: var(--border-color-dark);
628
  background-color: var(--card-background-dark);
629
  color: var(--text-color-dark);
630
+ box-shadow: inset 0 1px 3px rgba(0,0,0,0.2);
631
  }
632
  .modal-buttons {
633
+ margin-top: 30px;
634
  display: flex;
635
  justify-content: flex-end;
636
+ gap: 15px;
637
  }
638
  .modal-buttons .product-button {
639
  width: auto;
640
+ padding: 12px 25px;
641
  }
642
  .clear-cart {
643
  background-color: var(--danger-color);
644
  }
645
  .clear-cart:hover {
646
  background-color: var(--danger-dark-color);
647
+ box-shadow: 0 6px 15px rgba(239, 68, 68, 0.4);
648
  }
649
  .order-button {
650
  background-color: var(--accent-color);
651
  }
652
  .order-button:hover {
653
  background-color: var(--accent-dark-color);
654
+ box-shadow: 0 6px 15px rgba(16, 185, 129, 0.4);
655
  }
656
 
657
  .swiper-container {
658
+ max-width: 500px;
659
+ margin: 0 auto 30px;
660
+ border-radius: var(--border-radius-large);
661
  overflow: hidden;
662
+ box-shadow: var(--shadow-light);
663
  }
664
  body.dark-mode .swiper-container {
665
+ box-shadow: var(--shadow-dark);
666
  }
667
  .swiper-slide {
668
+ background-color: #f7f7f7;
669
  display: flex;
670
  justify-content: center;
671
  align-items: center;
672
+ min-height: 300px;
673
  }
674
  body.dark-mode .swiper-slide {
675
+ background-color: #343e4d;
676
  }
677
  .swiper-slide img, .swiper-slide video {
678
  max-width: 100%;
679
+ max-height: 350px;
680
  object-fit: contain;
681
  }
682
  .swiper-button-next, .swiper-button-prev {
683
  color: var(--primary-color) !important;
684
+ background-color: rgba(255,255,255,0.9);
685
  border-radius: 50%;
686
+ width: 45px;
687
+ height: 45px;
688
  display: flex;
689
  align-items: center;
690
  justify-content: center;
691
  transition: background-color 0.3s;
692
+ font-size: 1.8rem;
693
  }
694
  .swiper-button-next:hover, .swiper-button-prev:hover {
695
+ background-color: #fff;
696
  }
697
  body.dark-mode .swiper-button-next, body.dark-mode .swiper-button-prev {
698
+ background-color: rgba(45, 55, 72, 0.9);
699
  }
700
  body.dark-mode .swiper-button-next:hover, body.dark-mode .swiper-button-prev:hover {
701
+ background-color: #2d3748;
702
  }
703
  .swiper-pagination-bullet {
704
  background-color: var(--primary-color) !important;
705
  }
706
 
707
+ .product-detail-modal-inner {
708
+ background: transparent;
709
+ padding: 0;
710
+ margin: 0;
711
+ box-shadow: none;
712
+ position: static;
 
 
 
713
  }
714
+ body.dark-mode .product-detail-modal-inner {
715
+ background: transparent;
716
+ box-shadow: none;
 
717
  }
718
+ .product-detail-modal-inner h2 {
719
  font-size: 2.2rem;
720
  font-weight: 700;
721
  margin-bottom: 25px;
722
  text-align: center;
723
  color: var(--text-color-light);
724
  }
725
+ body.dark-mode .product-detail-modal-inner h2 {
726
  color: var(--text-color-dark);
727
  }
728
+ .product-detail-modal-inner p {
729
  margin-bottom: 10px;
730
  font-size: 1rem;
731
  }
732
+ .product-detail-modal-inner p strong {
733
  color: var(--text-color-light);
734
  }
735
+ body.dark-mode .product-detail-modal-inner p strong {
736
  color: var(--text-color-dark);
737
  }
738
+ .product-detail-modal-inner .price {
739
  font-size: 1.8rem;
740
  color: var(--danger-color);
741
  font-weight: 700;
742
  margin-bottom: 20px;
743
  text-align: center;
744
  }
745
+ .product-detail-modal-inner .description {
746
  margin-bottom: 20px;
747
  white-space: pre-wrap;
748
  }
749
+ .product-detail-modal-actions {
750
  display: flex;
751
  justify-content: center;
752
  gap: 15px;
753
  margin-top: 30px;
754
  }
755
+ .product-detail-modal-actions .product-button {
756
  width: auto;
757
  padding: 12px 25px;
758
  }
 
 
 
 
 
759
 
760
  @media (max-width: 768px) {
761
+ .container {
762
+ padding: 15px;
763
+ }
764
  .header h1 {
765
+ font-size: 1.6rem;
766
  }
767
  .category-filter {
768
  font-size: 0.85rem;
769
  padding: 8px 15px;
770
  }
771
  .products-grid {
772
+ grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
773
+ gap: 18px;
774
  }
775
  .product h2 {
776
+ font-size: 1.05rem;
777
  }
778
  .product-price {
779
+ font-size: 1.15rem;
780
  }
781
  .product-description {
782
+ font-size: 0.8rem;
783
  }
784
  .product-button {
785
+ font-size: 0.9rem;
786
+ padding: 10px;
787
  }
788
  #cart-button {
789
+ width: 50px;
790
+ height: 50px;
791
+ font-size: 1.2rem;
792
+ bottom: 20px;
793
+ right: 20px;
794
  }
795
  .modal-content {
796
  margin: 20px auto;
797
+ padding: 25px;
798
+ max-height: 95vh;
799
  }
800
  .close {
801
+ font-size: 1.6rem;
802
+ top: 15px;
803
+ right: 18px;
804
  }
805
  .modal h2 {
806
+ font-size: 1.6rem;
807
  }
808
  .cart-item img {
809
+ width: 55px;
810
+ height: 55px;
 
 
 
 
 
811
  }
812
+ .product-detail-modal-inner h2 {
813
+ font-size: 2rem;
814
  }
815
+ .product-detail-modal-inner .price {
816
+ font-size: 1.6rem;
817
  }
818
+ .product-detail-modal-actions {
819
  flex-direction: column;
820
  gap: 10px;
821
  }
 
828
  products = data.get('products', [])
829
  categories = data.get('categories', [])
830
 
831
+ products.sort(key=lambda p: p['name'].lower())
832
+
833
  return render_template_string('''
834
  <!DOCTYPE html>
835
  <html lang="ru">
836
  <head>
837
  <meta charset="UTF-8">
838
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
839
+ <title>ZZIRIX - сотовые аксессуары оптом</title>
840
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
841
  <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
842
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.css">
 
875
  data-name="{{ product['name']|lower }}"
876
  data-description="{{ product['description']|lower }}"
877
  data-category="{{ product.get('category', 'Без категории')|lower }}">
878
+ <a href="javascript:void(0)" onclick="openProductDetailModal('{{ product.id }}')" class="product-image">
879
  {% if product.get('photos') and product['photos']|length > 0 %}
880
  {% set filename = product['photos'][0] %}
881
  {% if filename.endswith(('.mp4', '.mov', '.webm')) %}
 
895
  <div class="product-price">{{ "%.2f"|format(product['price']) }} с</div>
896
  <p class="product-description">{{ product['description'][:50] }}{% if product['description']|length > 50 %}...{% endif %}</p>
897
  <div class="product-actions">
898
+ <button class="product-button" onclick="openProductDetailModal('{{ product.id }}')">Подробнее</button>
899
  <button class="product-button add-to-cart" onclick="openQuantityModal('{{ product.id }}')">В корзину</button>
900
  </div>
901
  </div>
 
933
  </div>
934
  </div>
935
 
936
+ <div id="productDetailModal" class="modal">
937
+ <div class="modal-content">
938
+ <span class="close" onclick="closeModal('productDetailModal')">×</span>
939
+ <div class="product-detail-modal-inner">
940
+ <h2 id="detailProductName"></h2>
941
+ <div class="swiper-container" style="max-width: 500px; margin: 0 auto 30px;">
942
+ <div class="swiper-wrapper" id="detailProductMedia">
943
+ </div>
944
+ <div class="swiper-pagination"></div>
945
+ <div class="swiper-button-next"></div>
946
+ <div class="swiper-button-prev"></div>
947
+ </div>
948
+ <p><strong>Категория:</strong> <span id="detailProductCategory" style="color: var(--primary-color);"></span></p>
949
+ <p class="price" id="detailProductPrice"></p>
950
+ <p class="description"><strong>Описание:</strong> <span id="detailProductDescription"></span></p>
951
+ <p><strong>Доступные цвета:</strong> <span id="detailProductColors" style="color: var(--secondary-text-color-light);"></span></p>
952
+ <p><strong>Доступные модели:</strong> <span id="detailProductModels" style="color: var(--secondary-text-color-light);"></span></p>
953
+ <div class="product-detail-modal-actions">
954
+ <button class="product-button add-to-cart" id="detailAddToCartButton">В корзину</button>
955
+ </div>
956
+ </div>
957
+ </div>
958
+ </div>
959
+
960
  <button id="cart-button" onclick="openCartModal()">
961
  <i class="fas fa-shopping-cart"></i>
962
  <span id="cart-count" class="cart-count">0</span>
 
966
  <script>
967
  const products = {{ products|tojson }};
968
  let selectedProductId = null;
969
+ let detailSwiperInstance = null;
970
 
971
  function toggleTheme() {
972
  document.body.classList.toggle('dark-mode');
 
987
 
988
  function closeModal(modalId) {
989
  document.getElementById(modalId).style.display = "none";
990
+ if (modalId === 'productDetailModal' && detailSwiperInstance) {
991
+ detailSwiperInstance.destroy(true, true);
992
+ detailSwiperInstance = null;
993
+ }
994
  }
995
 
996
  function openQuantityModal(productId) {
 
1053
  let cart = JSON.parse(localStorage.getItem('cart') || '[]');
1054
  const product = products.find(p => p.id === selectedProductId);
1055
 
 
1056
  const cartItemId = `${product.id}-${color}-${model}`;
1057
  const existingItem = cart.find(item => item.cartId === cartItemId);
1058
 
 
1060
  existingItem.quantity += quantity;
1061
  } else {
1062
  cart.push({
1063
+ cartId: cartItemId,
1064
+ productId: product.id,
1065
  name: product.name,
1066
  price: product.price,
1067
  photo: product.photos && product.photos.length > 0 ? product.photos[0] : '',
 
1098
  <img src="${photoSrc}" alt="${item.name}">
1099
  <div class="cart-item-details">
1100
  <strong>${item.name}</strong>
1101
+ <p>${item.price.toFixed(2)} с × ${item.quantity} (Цвет: ${item.color}, Модель: ${item.model})</p>
1102
  </div>
1103
+ <span class="cart-item-total">${itemTotal.toFixed(2)} с</span>
1104
  </div>
1105
  `;
1106
  }).join('');
 
1124
  });
1125
  orderText += `*Итого к оплате: ${total.toFixed(2)} с*%0A%0AЖду подтверждения заказа.`;
1126
  window.open(`https://api.whatsapp.com/send?phone=996705665777&text=${orderText}`, '_blank');
1127
+ clearCart();
1128
  }
1129
 
1130
  function clearCart() {
 
1136
  window.onclick = function(event) {
1137
  if (event.target.classList.contains('modal')) {
1138
  event.target.style.display = "none";
1139
+ if (detailSwiperInstance) {
1140
+ detailSwiperInstance.destroy(true, true);
1141
+ detailSwiperInstance = null;
1142
+ }
1143
  }
1144
  }
1145
 
 
1167
  });
1168
  }
1169
 
1170
+ function openProductDetailModal(productId) {
1171
+ selectedProductId = productId;
1172
+ const product = products.find(p => p.id === productId);
1173
+ if (!product) {
1174
+ alert('Ошибка: Товар не найден.');
1175
+ return;
1176
+ }
1177
+
1178
+ document.getElementById('detailProductName').textContent = product.name;
1179
+ document.getElementById('detailProductCategory').textContent = product.category || 'Без категории';
1180
+ document.getElementById('detailProductPrice').textContent = `${product.price.toFixed(2)} с`;
1181
+ document.getElementById('detailProductDescription').textContent = product.description;
1182
+ document.getElementById('detailProductColors').textContent = product.colors && product.colors.length > 0 ? product.colors.join(', ') : 'Нет цветов';
1183
+ document.getElementById('detailProductModels').textContent = product.models && product.models.length > 0 ? product.models.join(', ') : 'Нет моделей';
1184
+ document.getElementById('detailAddToCartButton').onclick = () => openQuantityModal(product.id);
1185
+
1186
+ const mediaContainer = document.getElementById('detailProductMedia');
1187
+ mediaContainer.innerHTML = '';
1188
+ if (product.photos && product.photos.length > 0) {
1189
+ product.photos.forEach(filename => {
1190
+ const slide = document.createElement('div');
1191
+ slide.className = 'swiper-slide swiper-zoom-container';
1192
+ slide.style = "display: flex; justify-content: center; align-items: center; border-radius: 10px;";
1193
+
1194
+ const mediaUrl = `https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/${filename}`;
1195
+ if (filename.endsWith(('.mp4', '.mov', '.webm'))) {
1196
+ slide.innerHTML = `<video controls preload="metadata" style="max-width: 100%; max-height: 350px; object-fit: contain;"><source src="${mediaUrl}" type="video/mp4"></video>`;
1197
+ } else {
1198
+ slide.innerHTML = `<img src="${mediaUrl}" alt="${product.name}" style="max-width: 100%; max-height: 350px; object-fit: contain;">`;
1199
+ }
1200
+ mediaContainer.appendChild(slide);
1201
+ });
1202
+ } else {
1203
+ const slide = document.createElement('div');
1204
+ slide.className = 'swiper-slide swiper-zoom-container';
1205
+ slide.style = "display: flex; justify-content: center; align-items: center; border-radius: 10px;";
1206
+ slide.innerHTML = `<img src="https://via.placeholder.com/300x300?text=No+Image" alt="No Image">`;
1207
+ mediaContainer.appendChild(slide);
1208
+ }
1209
+
1210
+ document.getElementById('productDetailModal').style.display = 'block';
1211
+
1212
+ if (detailSwiperInstance) {
1213
+ detailSwiperInstance.destroy(true, true);
1214
+ }
1215
+ detailSwiperInstance = new Swiper('#productDetailModal .swiper-container', {
1216
+ slidesPerView: 1,
1217
+ spaceBetween: 20,
1218
+ loop: true,
1219
+ grabCursor: true,
1220
+ pagination: { el: '#productDetailModal .swiper-pagination', clickable: true },
1221
+ navigation: { nextEl: '#productDetailModal .swiper-button-next', prevEl: '#productDetailModal .swiper-button-prev' },
1222
+ zoom: { maxRatio: 3 },
1223
+ on: {
1224
+ init: function () {
1225
+ this.slides.forEach(slide => {
1226
+ const img = slide.querySelector('img');
1227
+ if (img && !img.complete) {
1228
+ img.onload = () => detailSwiperInstance.update();
1229
+ }
1230
+ const video = slide.querySelector('video');
1231
+ if (video) {
1232
+ video.onloadedmetadata = () => detailSwiperInstance.update();
1233
+ }
1234
+ });
1235
+ },
1236
+ resize: function() {
1237
+ this.update();
1238
+ }
1239
+ }
1240
+ });
1241
+ detailSwiperInstance.update();
1242
+ }
1243
+
1244
  updateCartButton();
1245
  </script>
1246
  </body>
 
1259
  categories = data.get('categories', [])
1260
 
1261
  products = [p for p in products_all if p.get('category', '').lower() == category_name.lower()]
1262
+ products.sort(key=lambda p: p['name'].lower())
1263
 
1264
  return render_template_string('''
1265
  <!DOCTYPE html>
 
1294
  <div class="filters-container">
1295
  <button class="category-filter" data-category="all" onclick="window.location.href='{{ url_for('catalog') }}'">Все категории</button>
1296
  {% for cat in categories %}
1297
+ <button class="category-filter {% if cat | lower == category_name | lower %}active{% endif %}" data-category="{{ cat | lower }}" onclick="window.location.href='{{ url_for('catalog_by_category', category_name=cat | lower) }}'">{{ cat }}</button>
1298
  {% endfor %}
1299
  </div>
1300
  <div class="search-container">
 
1306
  data-name="{{ product['name']|lower }}"
1307
  data-description="{{ product['description']|lower }}"
1308
  data-category="{{ product.get('category', 'Без категории')|lower }}">
1309
+ <a href="javascript:void(0)" onclick="openProductDetailModal('{{ product.id }}')" class="product-image">
1310
  {% if product.get('photos') and product['photos']|length > 0 %}
1311
  {% set filename = product['photos'][0] %}
1312
  {% if filename.endswith(('.mp4', '.mov', '.webm')) %}
 
1326
  <div class="product-price">{{ "%.2f"|format(product['price']) }} с</div>
1327
  <p class="product-description">{{ product['description'][:50] }}{% if product['description']|length > 50 %}...{% endif %}</p>
1328
  <div class="product-actions">
1329
+ <button class="product-button" onclick="openProductDetailModal('{{ product.id }}')">Подробнее</button>
1330
  <button class="product-button add-to-cart" onclick="openQuantityModal('{{ product.id }}')">В корзину</button>
1331
  </div>
1332
  </div>
 
1364
  </div>
1365
  </div>
1366
 
1367
+ <div id="productDetailModal" class="modal">
1368
+ <div class="modal-content">
1369
+ <span class="close" onclick="closeModal('productDetailModal')">×</span>
1370
+ <div class="product-detail-modal-inner">
1371
+ <h2 id="detailProductName"></h2>
1372
+ <div class="swiper-container" style="max-width: 500px; margin: 0 auto 30px;">
1373
+ <div class="swiper-wrapper" id="detailProductMedia">
1374
+ </div>
1375
+ <div class="swiper-pagination"></div>
1376
+ <div class="swiper-button-next"></div>
1377
+ <div class="swiper-button-prev"></div>
1378
+ </div>
1379
+ <p><strong>Категория:</strong> <span id="detailProductCategory" style="color: var(--primary-color);"></span></p>
1380
+ <p class="price" id="detailProductPrice"></p>
1381
+ <p class="description"><strong>Описание:</strong> <span id="detailProductDescription"></span></p>
1382
+ <p><strong>Доступные цвета:</strong> <span id="detailProductColors" style="color: var(--secondary-text-color-light);"></span></p>
1383
+ <p><strong>Доступные модели:</strong> <span id="detailProductModels" style="color: var(--secondary-text-color-light);"></span></p>
1384
+ <div class="product-detail-modal-actions">
1385
+ <button class="product-button add-to-cart" id="detailAddToCartButton">В корзину</button>
1386
+ </div>
1387
+ </div>
1388
+ </div>
1389
+ </div>
1390
+
1391
  <button id="cart-button" onclick="openCartModal()">
1392
  <i class="fas fa-shopping-cart"></i>
1393
  <span id="cart-count" class="cart-count">0</span>
 
1395
 
1396
  <script src="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.js"></script>
1397
  <script>
1398
+ const products = {{ products_all|tojson }};
1399
  let selectedProductId = null;
1400
+ let detailSwiperInstance = null;
1401
 
1402
  function toggleTheme() {
1403
  document.body.classList.toggle('dark-mode');
 
1418
 
1419
  function closeModal(modalId) {
1420
  document.getElementById(modalId).style.display = "none";
1421
+ if (modalId === 'productDetailModal' && detailSwiperInstance) {
1422
+ detailSwiperInstance.destroy(true, true);
1423
+ detailSwiperInstance = null;
1424
+ }
1425
  }
1426
 
1427
  function openQuantityModal(productId) {
 
1484
  let cart = JSON.parse(localStorage.getItem('cart') || '[]');
1485
  const product = products.find(p => p.id === selectedProductId);
1486
 
 
1487
  const cartItemId = `${product.id}-${color}-${model}`;
1488
  const existingItem = cart.find(item => item.cartId === cartItemId);
1489
 
 
1491
  existingItem.quantity += quantity;
1492
  } else {
1493
  cart.push({
1494
+ cartId: cartItemId,
1495
+ productId: product.id,
1496
  name: product.name,
1497
  price: product.price,
1498
  photo: product.photos && product.photos.length > 0 ? product.photos[0] : '',
 
1555
  });
1556
  orderText += `*Итого к оплате: ${total.toFixed(2)} с*%0A%0AЖду подтверждения заказа.`;
1557
  window.open(`https://api.whatsapp.com/send?phone=996705665777&text=${orderText}`, '_blank');
1558
+ clearCart();
1559
  }
1560
 
1561
  function clearCart() {
 
1567
  window.onclick = function(event) {
1568
  if (event.target.classList.contains('modal')) {
1569
  event.target.style.display = "none";
1570
+ if (detailSwiperInstance) {
1571
+ detailSwiperInstance.destroy(true, true);
1572
+ detailSwiperInstance = null;
1573
+ }
1574
  }
1575
  }
1576
 
1577
  document.getElementById('search-input').addEventListener('input', filterProducts);
 
 
 
 
 
 
 
 
 
 
1578
 
1579
  function filterProducts() {
1580
  const searchTerm = document.getElementById('search-input').value.toLowerCase().trim();
1581
+ const activeCategory = "{{ category_name | lower }}";
1582
 
1583
  document.querySelectorAll('.product').forEach(product => {
1584
  const name = product.getAttribute('data-name');
1585
  const description = product.getAttribute('data-description');
 
1586
 
1587
  const matchesSearch = (name && name.includes(searchTerm)) || (description && description.includes(searchTerm));
1588
 
 
1589
  product.style.display = matchesSearch ? 'flex' : 'none';
1590
  });
1591
  }
1592
 
1593
+ function openProductDetailModal(productId) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1594
  selectedProductId = productId;
1595
  const product = products.find(p => p.id === productId);
1596
  if (!product) {
1597
  alert('Ошибка: Товар не найден.');
1598
  return;
1599
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1600
 
1601
+ document.getElementById('detailProductName').textContent = product.name;
1602
+ document.getElementById('detailProductCategory').textContent = product.category || 'Без категории';
1603
+ document.getElementById('detailProductPrice').textContent = `${product.price.toFixed(2)} с`;
1604
+ document.getElementById('detailProductDescription').textContent = product.description;
1605
+ document.getElementById('detailProductColors').textContent = product.colors && product.colors.length > 0 ? product.colors.join(', ') : 'Нет цветов';
1606
+ document.getElementById('detailProductModels').textContent = product.models && product.models.length > 0 ? product.models.join(', ') : 'Нет моделей';
1607
+ document.getElementById('detailAddToCartButton').onclick = () => openQuantityModal(product.id);
1608
+
1609
+ const mediaContainer = document.getElementById('detailProductMedia');
1610
+ mediaContainer.innerHTML = '';
1611
+ if (product.photos && product.photos.length > 0) {
1612
+ product.photos.forEach(filename => {
1613
+ const slide = document.createElement('div');
1614
+ slide.className = 'swiper-slide swiper-zoom-container';
1615
+ slide.style = "display: flex; justify-content: center; align-items: center; border-radius: 10px;";
1616
+
1617
+ const mediaUrl = `https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/${filename}`;
1618
+ if (filename.endsWith(('.mp4', '.mov', '.webm'))) {
1619
+ slide.innerHTML = `<video controls preload="metadata" style="max-width: 100%; max-height: 350px; object-fit: contain;"><source src="${mediaUrl}" type="video/mp4"></video>`;
1620
+ } else {
1621
+ slide.innerHTML = `<img src="${mediaUrl}" alt="${product.name}" style="max-width: 100%; max-height: 350px; object-fit: contain;">`;
1622
+ }
1623
+ mediaContainer.appendChild(slide);
1624
  });
1625
  } else {
1626
+ const slide = document.createElement('div');
1627
+ slide.className = 'swiper-slide swiper-zoom-container';
1628
+ slide.style = "display: flex; justify-content: center; align-items: center; border-radius: 10px;";
1629
+ slide.innerHTML = `<img src="https://via.placeholder.com/300x300?text=No+Image" alt="No Image">`;
1630
+ mediaContainer.appendChild(slide);
1631
  }
 
 
 
 
 
 
 
 
 
 
 
1632
 
1633
+ document.getElementById('productDetailModal').style.display = 'block';
 
 
 
 
1634
 
1635
+ if (detailSwiperInstance) {
1636
+ detailSwiperInstance.destroy(true, true);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1637
  }
1638
+ detailSwiperInstance = new Swiper('#productDetailModal .swiper-container', {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1639
  slidesPerView: 1,
1640
  spaceBetween: 20,
1641
  loop: true,
1642
  grabCursor: true,
1643
+ pagination: { el: '#productDetailModal .swiper-pagination', clickable: true },
1644
+ navigation: { nextEl: '#productDetailModal .swiper-button-next', prevEl: '#productDetailModal .swiper-button-prev' },
1645
  zoom: { maxRatio: 3 },
1646
  on: {
1647
  init: function () {
1648
  this.slides.forEach(slide => {
1649
  const img = slide.querySelector('img');
1650
  if (img && !img.complete) {
1651
+ img.onload = () => detailSwiperInstance.update();
1652
  }
1653
  const video = slide.querySelector('video');
1654
  if (video) {
1655
+ video.onloadedmetadata = () => detailSwiperInstance.update();
1656
  }
1657
  });
1658
  },
 
1661
  }
1662
  }
1663
  });
1664
+ detailSwiperInstance.update();
1665
  }
1666
+
1667
  updateCartButton();
1668
  </script>
1669
  </body>
1670
  </html>
1671
  ''',
1672
+ products=products,
1673
  categories=categories,
1674
+ category_name=category_name,
1675
+ products_all=products_all,
1676
  repo_id=REPO_ID,
1677
+ LOGO_URL=LOGO_URL
 
1678
  )
1679
 
1680
  @app.route('/admin', methods=['GET', 'POST'])
 
1811
  uploads_dir = 'uploads'
1812
  os.makedirs(uploads_dir, exist_ok=True)
1813
 
 
1814
  for old_filename in product_to_edit.get('photos', []):
1815
  try:
1816
  api.delete_file(path_in_repo=f"photos/{old_filename}", repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE)
 
1852
  product_to_delete = next((p for p in products if p.get('id') == product_id), None)
1853
 
1854
  if product_to_delete:
 
1855
  for filename in product_to_delete.get('photos', []):
1856
  try:
1857
  api.delete_file(path_in_repo=f"photos/{filename}", repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE)
 
1866
  flash('Ошибка: Товар не найден для удаления.', 'error')
1867
  return redirect(url_for('admin'))
1868
 
1869
+ products.sort(key=lambda p: p['name'].lower())
1870
+
1871
  return render_template_string('''
1872
  <!DOCTYPE html>
1873
  <html lang="ru">
 
1920
  form {
1921
  background: var(--card-background-light);
1922
  padding: 30px;
1923
+ border-radius: var(--border-radius-large);
1924
  box-shadow: var(--shadow-light);
1925
  margin-bottom: 30px;
1926
  transition: background 0.3s, box-shadow 0.3s;
 
1944
  padding: 12px;
1945
  margin-bottom: 10px;
1946
  border: 1px solid var(--border-color-light);
1947
+ border-radius: var(--border-radius-medium);
1948
  font-size: 1rem;
1949
  transition: all 0.3s ease;
1950
  background-color: var(--card-background-light);
 
1963
  button {
1964
  padding: 12px 22px;
1965
  border: none;
1966
+ border-radius: var(--border-radius-medium);
1967
  background-color: var(--primary-color);
1968
  color: white;
1969
  font-weight: 500;
 
1992
  .product-item, .category-item {
1993
  background: var(--card-background-light);
1994
  padding: 20px;
1995
+ border-radius: var(--border-radius-large);
1996
  box-shadow: var(--shadow-light);
1997
  transition: background 0.3s, box-shadow 0.3s;
1998
  }
 
2021
  margin-top: 20px;
2022
  padding: 20px;
2023
  background: var(--background-light);
2024
+ border-radius: var(--border-radius-medium);
2025
  box-shadow: inset 0 2px 5px rgba(0,0,0,0.05);
2026
  }
2027
  body.dark-mode .edit-form {
 
2042
  background-color: #ccc;
2043
  color: #555;
2044
  padding: 8px 12px;
2045
+ border-radius: var(--border-radius-small);
2046
  font-size: 0.8rem;
2047
  margin-top: 0;
2048
  flex-shrink: 0;
 
2069
  margin-top: 10px;
2070
  margin-bottom: 15px;
2071
  }
2072
+ .product-photos-preview img, .product-photos-preview video {
2073
  max-width: 90px;
2074
  height: auto;
2075
+ border-radius: var(--border-radius-small);
2076
  border: 1px solid var(--border-color-light);
2077
  box-shadow: 0 2px 8px rgba(0,0,0,0.05);
2078
  }
2079
+ body.dark-mode .product-photos-preview img, body.dark-mode .product-photos-preview video {
2080
  border-color: var(--border-color-dark);
2081
  box-shadow: 0 2px 8px rgba(0,0,0,0.15);
2082
  }
 
2103
  width: 100%;
2104
  max-width: 400px;
2105
  margin-left: 0;
2106
+ border: 1px solid var(--border-color-light);
2107
+ box-shadow: inset 0 1px 3px rgba(0,0,0,0.05);
2108
+ }
2109
+ body.dark-mode #admin-search-input {
2110
+ border-color: var(--border-color-dark);
2111
+ box-shadow: inset 0 1px 3px rgba(0,0,0,0.2);
2112
  }
2113
  </style>
2114
  </head>
 
2224
  alt="{{ product['name'] }}"
2225
  loading="lazy"
2226
  controls
2227
+ style="max-width: 90px; height: auto; border-radius: var(--border-radius-small); border: 1px solid var(--border-color-light); box-shadow: 0 2px 8px rgba(0,0,0,0.05);">
2228
  </video>
2229
  {% else %}
2230
  <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo }}?r={{ random.randint(1,1000) }}"
 
2336
  @app.route('/backup', methods=['POST'])
2337
  def backup():
2338
  try:
2339
+ data = load_data()
2340
  save_data(data)
2341
  flash('Резервная копия успешно создана и загружена.', 'success')
2342
  return redirect(url_for('admin'))
 
2348
  @app.route('/download', methods=['GET'])
2349
  def download():
2350
  try:
2351
+ download_db_from_hf()
2352
  if os.path.exists(DATA_FILE):
2353
  return send_file(DATA_FILE, as_attachment=True, mimetype='application/json', download_name='data_zzirix_backup.json')
2354
  flash('Файл базы данных не найден после попытки скачивания.', 'error')
 
2365
  logging.info("Начальная проверка и обновление структуры данных...")
2366
  try:
2367
  current_data = load_data()
2368
+ save_data(current_data)
2369
  logging.info("Структура данных проверена и при необходимости обновлена.")
2370
  except Exception as e:
2371
  logging.error(f"Ошибка во время начальной проверки/обновления структуры данных: {e}")
 
2373
  backup_thread = threading.Thread(target=periodic_backup, daemon=True)
2374
  backup_thread.start()
2375
  logging.info("Запуск Flask приложения...")
2376
+ app.run(debug=True, host='0.0.0.0', port=7860)