Kgshop commited on
Commit
9ab6055
·
verified ·
1 Parent(s): bdab0b0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +286 -1340
app.py CHANGED
@@ -1,108 +1,89 @@
1
- from flask import Flask, render_template_string, request, redirect, url_for
2
  import json
3
- import os
4
  import logging
5
  import threading
6
  import time
7
  from datetime import datetime
8
- from huggingface_hub import HfApi, hf_hub_download
9
- from huggingface_hub.utils import RepositoryNotFoundError
10
  from werkzeug.utils import secure_filename
11
 
12
  app = Flask(__name__)
13
- DATA_FILE = 'data_kanc.json'
14
 
15
- # Настройки Hugging Face
16
- REPO_ID = "Kgshop/clients"
17
- HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
18
- HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
19
 
20
- # Настройка логирования
21
- logging.basicConfig(level=logging.DEBUG)
 
 
 
22
 
 
 
 
 
23
  def load_data():
24
  try:
25
- download_db_from_hf()
26
- with open(DATA_FILE, 'r', encoding='utf-8') as file:
27
- data = json.load(file)
28
- logging.info("Данные успешно загружены из JSON")
29
- if not isinstance(data, dict) or 'products' not in data or 'categories' not in data:
30
- return {'products': [], 'categories': [] if not isinstance(data, list) else data}
31
- return data
32
- except FileNotFoundError:
33
- logging.warning("Локальный файл базы данных не найден после скачивания.")
34
- return {'products': [], 'categories': []}
35
- except json.JSONDecodeError:
36
- logging.error("Ошибка: Невозможно декодировать JSON файл.")
37
- return {'products': [], 'categories': []}
38
- except RepositoryNotFoundError:
39
- logging.error("Репозиторий не найден. Создание локальной базы данных.")
40
- return {'products': [], 'categories': []}
41
  except Exception as e:
42
- logging.error(f"Произошла ошибка при загрузке данных: {e}")
43
- return {'products': [], 'categories': []}
44
 
 
45
  def save_data(data):
46
- try:
47
- with open(DATA_FILE, 'w', encoding='utf-8') as file:
48
- json.dump(data, file, ensure_ascii=False, indent=4)
49
- logging.info("Данные успешно сохранены в JSON")
 
 
 
 
 
 
 
 
 
 
 
50
  upload_db_to_hf()
51
- except Exception as e:
52
- logging.error(f"Ошибка при сохранении данных: {e}")
53
- raise
54
 
55
  def upload_db_to_hf():
56
- try:
57
- api = HfApi()
58
- api.upload_file(
59
- path_or_fileobj=DATA_FILE,
60
- path_in_repo=DATA_FILE,
61
- repo_id=REPO_ID,
62
- repo_type="dataset",
63
- token=HF_TOKEN_WRITE,
64
- commit_message=f"Автоматическое резервное копирование базы данных {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
65
- )
66
- logging.info("Резервная копия JSON базы успешно загружена на Hugging Face.")
67
- except Exception as e:
68
- logging.error(f"Ошибка при загрузке резервной копии: {e}")
69
 
70
  def download_db_from_hf():
71
- try:
72
- hf_hub_download(
73
- repo_id=REPO_ID,
74
- filename=DATA_FILE,
75
- repo_type="dataset",
76
- token=HF_TOKEN_READ,
77
- local_dir=".",
78
- local_dir_use_symlinks=False
79
- )
80
- logging.info("JSON база успешно скачана из Hugging Face.")
81
- except RepositoryNotFoundError as e:
82
- logging.error(f"Репозиторий не найден: {e}")
83
- raise
84
- except Exception as e:
85
- logging.error(f"Ошибка при скачивании JSON базы: {e}")
86
- raise
87
-
88
- def periodic_backup():
89
- while True:
90
- upload_db_to_hf()
91
- time.sleep(800)
92
 
93
  @app.route('/')
94
- def catalog():
95
  data = load_data()
96
  products = data['products']
97
- categories = data['categories']
98
 
99
- catalog_html = '''
100
  <!DOCTYPE html>
101
  <html lang="ru">
102
  <head>
103
  <meta charset="UTF-8">
104
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
105
- <title>Канцелярия Оптом</title>
106
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
107
  <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
108
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.css">
@@ -158,13 +139,6 @@ def catalog():
158
  body.dark-mode .theme-toggle {
159
  color: #bbb;
160
  }
161
- .filters-container {
162
- margin: 20px 0;
163
- display: flex;
164
- flex-wrap: wrap;
165
- gap: 10px;
166
- justify-content: center;
167
- }
168
  .search-container {
169
  margin: 20px 0;
170
  text-align: center;
@@ -184,26 +158,10 @@ def catalog():
184
  border-color: #526df2;
185
  box-shadow: 0 4px 15px rgba(82, 109, 242, 0.2);
186
  }
187
- .category-filter {
188
- padding: 10px 20px;
189
- border: 1px solid rgba(0, 0, 0, 0.1);
190
- border-radius: 25px;
191
- background-color: #fff;
192
- cursor: pointer;
193
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
194
- font-size: 0.9rem;
195
- font-weight: 500;
196
- }
197
- .category-filter.active, .category-filter:hover {
198
- background-color: #526df2;
199
- color: white;
200
- border-color: #526df2;
201
- box-shadow: 0 2px 10px rgba(82, 109, 242, 0.3);
202
- }
203
- body.dark-mode .category-filter {
204
- background-color: #2a2a3e;
205
- border-color: rgba(255, 255, 255, 0.1);
206
  color: #e0e0e0;
 
207
  }
208
  .products-grid {
209
  display: grid;
@@ -509,21 +467,28 @@ def catalog():
509
  font-size: 0.75rem;
510
  font-weight: 500;
511
  }
 
 
 
 
 
 
 
 
 
512
  </style>
513
  </head>
514
  <body>
515
  <div class="container">
516
  <div class="header">
517
- <h1>Канцелярия Оптом</h1>
518
  <button class="theme-toggle" onclick="toggleTheme()">
519
  <i class="fas fa-moon"></i>
520
  </button>
521
  </div>
522
- <div class="filters-container">
523
- <button class="category-filter active" data-category="all">Все категории</button>
524
- {% for category in categories %}
525
- <button class="category-filter" data-category="{{ category }}">{{ category }}</button>
526
- {% endfor %}
527
  </div>
528
  <div class="search-container">
529
  <input type="text" id="search-input" placeholder="Поиск товаров...">
@@ -604,18 +569,8 @@ def catalog():
604
  </div>
605
  </div>
606
 
607
- <!-- Address Modal -->
608
- <div id="addressModal" class="modal">
609
- <div class="modal-content">
610
- <span class="close" onclick="closeModal('addressModal')">×</span>
611
- <h2>Наш адрес</h2>
612
- <p>Рынок Дордой, Джунхай, 5 проход 505-506 контейнеры</p>
613
- </div>
614
- </div>
615
-
616
  <button id="cart-button" onclick="openCartModal()">🛒</button>
617
 
618
- <!-- Navigation Bar -->
619
  <div class="navbar">
620
  <a href="/" class="active">
621
  <i class="fas fa-home"></i>
@@ -633,10 +588,6 @@ def catalog():
633
  <i class="fas fa-tag"></i>
634
  Скидки
635
  </a>
636
- <a href="#" onclick="openAddressModal()">
637
- <i class="fas fa-map-marker-alt"></i>
638
- Адрес
639
- </a>
640
  <a href="https://api.whatsapp.com/send?phone=996500654659">
641
  <i class="fab fa-whatsapp"></i>
642
  WhatsApp
@@ -663,6 +614,19 @@ def catalog():
663
  document.querySelector('.theme-toggle i').classList.replace('fa-moon', 'fa-sun');
664
  }
665
 
 
 
 
 
 
 
 
 
 
 
 
 
 
666
  function openModal(index) {
667
  loadProductDetails(index);
668
  document.getElementById('productModal').style.display = "block";
@@ -827,6 +791,7 @@ def catalog():
827
  favoriteButton.classList.add('favorited');
828
  }
829
  localStorage.setItem('favorites', JSON.stringify(favorites));
 
830
  }
831
 
832
  function loadFavorites() {
@@ -839,43 +804,18 @@ def catalog():
839
  });
840
  }
841
 
842
- function openAddressModal() {
843
- document.getElementById('addressModal').style.display = 'block';
844
- }
845
-
846
  window.onclick = function(event) {
847
  if (event.target.className === 'modal') event.target.style.display = "none";
848
  }
849
 
850
- document.getElementById('search-input').addEventListener('input', filterProducts);
851
- document.querySelectorAll('.category-filter').forEach(filter => {
852
- filter.addEventListener('click', function() {
853
- document.querySelectorAll('.category-filter').forEach(f => f.classList.remove('active'));
854
- this.classList.add('active');
855
- filterProducts();
856
- });
857
- });
858
-
859
- function filterProducts() {
860
- const searchTerm = document.getElementById('search-input').value.toLowerCase();
861
- const activeCategory = document.querySelector('.category-filter.active').dataset.category;
862
- document.querySelectorAll('.product').forEach(product => {
863
- const name = product.getAttribute('data-name');
864
- const description = product.getAttribute('data-description');
865
- const category = product.getAttribute('data-category');
866
- const matchesSearch = name.includes(searchTerm) || description.includes(searchTerm);
867
- const matchesCategory = activeCategory === 'all' || category === activeCategory;
868
- product.style.display = matchesSearch && matchesCategory ? 'block' : 'none';
869
- });
870
- }
871
-
872
  updateCartButton();
873
  loadFavorites();
 
874
  </script>
875
  </body>
876
  </html>
877
  '''
878
- return render_template_string(catalog_html, products=products, categories=categories, repo_id=REPO_ID)
879
 
880
  @app.route('/categories')
881
  def categories_page():
@@ -888,7 +828,7 @@ def categories_page():
888
  <head>
889
  <meta charset="UTF-8">
890
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
891
- <title>Категории</title>
892
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
893
  <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
894
  <style>
@@ -943,43 +883,142 @@ def categories_page():
943
  body.dark-mode .theme-toggle {
944
  color: #bbb;
945
  }
946
- .categories-grid {
947
  display: grid;
948
- grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
949
  gap: 20px;
950
- padding: 10px;
951
  }
952
  .category-item {
953
- background: rgba(255, 255, 255, 0.8);
954
  border: 1px solid rgba(0, 0, 0, 0.05);
955
  border-radius: 15px;
956
- padding: 20px;
957
  text-align: center;
958
  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
959
- transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.3s ease, background 0.3s ease;
 
960
  text-decoration: none;
961
  color: #333;
962
  }
963
- body.dark-mode .category-item {
964
- background: rgba(42, 42, 62, 0.8);
965
- border-color: rgba(255, 255, 255, 0.05);
966
- color: #e0e0e0;
967
- }
968
  .category-item:hover {
969
  transform: translateY(-5px);
970
  box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
971
- background: rgba(255, 255, 255, 0.9);
972
- background-color: #526df2;
973
- color: white;
974
  }
975
- body.dark-mode .category-item:hover {
976
  background: rgba(42, 42, 62, 0.9);
977
- box-shadow: 0 8px 25px rgba(255, 255, 255, 0.1);
 
978
  }
979
  .category-item h2 {
980
  font-size: 1.2rem;
981
  font-weight: 500;
982
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
983
  .navbar {
984
  position: fixed;
985
  bottom: 0;
@@ -1021,29 +1060,36 @@ def categories_page():
1021
  <body>
1022
  <div class="container">
1023
  <div class="header">
1024
- <h1>Категории</h1>
1025
  <button class="theme-toggle" onclick="toggleTheme()">
1026
  <i class="fas fa-moon"></i>
1027
  </button>
1028
  </div>
1029
- <div class="categories-grid">
1030
  {% for category in categories %}
1031
- <a href="/category/{{ category }}" class="category-item">
1032
  <h2>{{ category }}</h2>
1033
  </a>
1034
  {% endfor %}
1035
  </div>
1036
  </div>
1037
 
1038
- <!-- Address Modal -->
1039
- <div id="addressModal" class="modal">
1040
  <div class="modal-content">
1041
- <span class="close" onclick="closeModal('addressModal')">×</span>
1042
- <h2>Наш адрес</h2>
1043
- <p>Рынок Дордой, Джунхай, 5 проход 505-506 контейнеры</p>
 
 
 
 
 
1044
  </div>
1045
  </div>
1046
 
 
 
1047
  <div class="navbar">
1048
  <a href="/">
1049
  <i class="fas fa-home"></i>
@@ -1061,658 +1107,24 @@ def categories_page():
1061
  <i class="fas fa-tag"></i>
1062
  Скидки
1063
  </a>
1064
- <a href="#" onclick="openAddressModal()">
1065
- <i class="fas fa-map-marker-alt"></i>
1066
- Адрес
1067
- </a>
1068
  <a href="https://api.whatsapp.com/send?phone=996500654659">
1069
  <i class="fab fa-whatsapp"></i>
1070
- WhatsApp
1071
- </a>
1072
- </div>
1073
-
1074
- <script>
1075
- function toggleTheme() {
1076
- document.body.classList.toggle('dark-mode');
1077
- const icon = document.querySelector('.theme-toggle i');
1078
- icon.classList.toggle('fa-moon');
1079
- icon.classList.toggle('fa-sun');
1080
- localStorage.setItem('theme', document.body.classList.contains('dark-mode') ? 'dark' : 'light');
1081
- }
1082
-
1083
- if (localStorage.getItem('theme') === 'dark') {
1084
- document.body.classList.add('dark-mode');
1085
- document.querySelector('.theme-toggle i').classList.replace('fa-moon', 'fa-sun');
1086
- }
1087
-
1088
- function openAddressModal() {
1089
- document.getElementById('addressModal').style.display = 'block';
1090
- }
1091
-
1092
- function closeModal(modalId) {
1093
- document.getElementById(modalId).style.display = "none";
1094
- }
1095
-
1096
- window.onclick = function(event) {
1097
- if (event.target.className === 'modal') event.target.style.display = "none";
1098
- }
1099
- </script>
1100
- </body>
1101
- </html>
1102
- '''
1103
- return render_template_string(categories_html, categories=categories)
1104
-
1105
- @app.route('/category/<category>')
1106
- def category_products(category):
1107
- data = load_data()
1108
- products = [p for p in data['products'] if p.get('category') == category]
1109
- categories = data['categories']
1110
-
1111
- category_html = '''
1112
- <!DOCTYPE html>
1113
- <html lang="ru">
1114
- <head>
1115
- <meta charset="UTF-8">
1116
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
1117
- <title>{{ category }}</title>
1118
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
1119
- <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
1120
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.css">
1121
- <style>
1122
- * {
1123
- margin: 0;
1124
- padding: 0;
1125
- box-sizing: border-box;
1126
- }
1127
- body {
1128
- font-family: 'Roboto', sans-serif;
1129
- background: linear-gradient(135deg, #f5f7fa, #e4e7eb);
1130
- color: #333;
1131
- line-height: 1.6;
1132
- transition: background 0.3s, color 0.3s;
1133
- padding-bottom: 60px;
1134
- }
1135
- body.dark-mode {
1136
- background: linear-gradient(135deg, #1a1a2e, #2a2a3e);
1137
- color: #e0e0e0;
1138
- }
1139
- .container {
1140
- max-width: 1400px;
1141
- margin: 0 auto;
1142
- padding: 20px;
1143
- }
1144
- .header {
1145
- display: flex;
1146
- justify-content: space-between;
1147
- align-items: center;
1148
- padding: 15px 0;
1149
- border-bottom: 1px solid rgba(0, 0, 0, 0.1);
1150
- }
1151
- .header h1 {
1152
- font-size: 2rem;
1153
- font-weight: 700;
1154
- letter-spacing: 1px;
1155
- background: linear-gradient(90deg, #526df2, #7a8ff5);
1156
- -webkit-background-clip: text;
1157
- -webkit-text-fill-color: transparent;
1158
- }
1159
- .theme-toggle {
1160
- background: none;
1161
- border: none;
1162
- font-size: 1.5rem;
1163
- cursor: pointer;
1164
- color: #666;
1165
- transition: color 0.3s ease;
1166
- }
1167
- .theme-toggle:hover {
1168
- color: #526df2;
1169
- }
1170
- body.dark-mode .theme-toggle {
1171
- color: #bbb;
1172
- }
1173
- .products-grid {
1174
- display: grid;
1175
- grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
1176
- gap: 20px;
1177
- padding: 10px;
1178
- }
1179
- .product {
1180
- background: rgba(255, 255, 255, 0.8);
1181
- border: 1px solid rgba(0, 0, 0, 0.05);
1182
- border-radius: 15px;
1183
- padding: 15px;
1184
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
1185
- transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.3s ease, background 0.3s ease;
1186
- cursor: pointer;
1187
- position: relative;
1188
- overflow: hidden;
1189
- }
1190
- .product:hover {
1191
- transform: translateY(-5px);
1192
- box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
1193
- background: rgba(255, 255, 255, 0.9);
1194
- }
1195
- body.dark-mode .product {
1196
- background: rgba(42, 42, 62, 0.8);
1197
- border-color: rgba(255, 255, 255, 0.05);
1198
- color: #e0e0e0;
1199
- }
1200
- body.dark-mode .product:hover {
1201
- background: rgba(42, 42, 62, 0.9);
1202
- box-shadow: 0 8px 25px rgba(255, 255, 255, 0.1);
1203
- }
1204
- .product-image {
1205
- width: 100%;
1206
- aspect-ratio: 1;
1207
- background-color: #fff;
1208
- border-radius: 10px;
1209
- overflow: hidden;
1210
- display: flex;
1211
- justify-content: center;
1212
- align-items: center;
1213
- transition: transform 0.3s ease;
1214
- }
1215
- body.dark-mode .product-image {
1216
- background-color: #333;
1217
- }
1218
- .product-image img {
1219
- max-width: 100%;
1220
- max-height: 100%;
1221
- object-fit: contain;
1222
- transition: transform 0.3s ease;
1223
- }
1224
- .product-image img:hover {
1225
- transform: scale(1.05);
1226
- }
1227
- .product h2 {
1228
- font-size: 1.1rem;
1229
- font-weight: 500;
1230
- margin: 10px 0;
1231
- text-align: center;
1232
- white-space: nowrap;
1233
- overflow: hidden;
1234
- text-overflow: ellipsis;
1235
- }
1236
- .product-price {
1237
- font-size: 1.2rem;
1238
- color: #e63946;
1239
- font-weight: 700;
1240
- text-align: center;
1241
- margin: 5px 0;
1242
- }
1243
- .product-price .wholesale {
1244
- font-size: 0.9rem;
1245
- color: #526df2;
1246
- font-weight: 500;
1247
- display: block;
1248
- margin-top: 5px;
1249
- }
1250
- .product-price .discount {
1251
- font-size: 0.9rem;
1252
- color: #2ecc71;
1253
- font-weight: 500;
1254
- display: block;
1255
- margin-top: 5px;
1256
- }
1257
- .product-description {
1258
- font-size: 0.85rem;
1259
- color: #666;
1260
- text-align: center;
1261
- margin-bottom: 15px;
1262
- overflow: hidden;
1263
- text-overflow: ellipsis;
1264
- white-space: nowrap;
1265
- }
1266
- body.dark-mode .product-description {
1267
- color: #bbb;
1268
- }
1269
- .product-button {
1270
- display: block;
1271
- width: 100%;
1272
- padding: 10px;
1273
- border: none;
1274
- border-radius: 25px;
1275
- background-color: #526df2;
1276
- color: white;
1277
- font-size: 0.9rem;
1278
- font-weight: 500;
1279
- cursor: pointer;
1280
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
1281
- margin: 5px 0;
1282
- text-align: center;
1283
- text-decoration: none;
1284
- }
1285
- .product-button:hover {
1286
- background-color: #3e55d1;
1287
- box-shadow: 0 4px 15px rgba(62, 85, 209, 0.4);
1288
- transform: translateY(-2px);
1289
- }
1290
- .add-to-cart {
1291
- background-color: #2ecc71;
1292
- }
1293
- .add-to-cart:hover {
1294
- background-color: #27ae60;
1295
- box-shadow: 0 4px 15px rgba(46, 204, 113, 0.4);
1296
- }
1297
- .favorite-button {
1298
- position: absolute;
1299
- top: 10px;
1300
- left: 10px;
1301
- background: none;
1302
- border: none;
1303
- font-size: 1.5rem;
1304
- cursor: pointer;
1305
- color: #666;
1306
- transition: color 0.3s ease;
1307
- }
1308
- .favorite-button.favorited {
1309
- color: #e63946;
1310
- }
1311
- .favorite-button:hover {
1312
- color: #e63946;
1313
- }
1314
- #cart-button {
1315
- position: fixed;
1316
- bottom: 80px;
1317
- right: 20px;
1318
- background-color: #e63946;
1319
- color: white;
1320
- border: none;
1321
- border-radius: 50%;
1322
- width: 60px;
1323
- height: 60px;
1324
- font-size: 1.5rem;
1325
- cursor: pointer;
1326
- display: none;
1327
- box-shadow: 0 4px 15px rgba(230, 57, 70, 0.4);
1328
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
1329
- z-index: 1000;
1330
- }
1331
- #cart-button:hover {
1332
- transform: scale(1.1);
1333
- }
1334
- .modal {
1335
- display: none;
1336
- position: fixed;
1337
- z-index: 1001;
1338
- left: 0;
1339
- top: 0;
1340
- width: 100%;
1341
- height: 100%;
1342
- background-color: rgba(0,0,0,0.5);
1343
- backdrop-filter: blur(5px);
1344
- }
1345
- .modal-content {
1346
- background: #fff;
1347
- margin: 5% auto;
1348
- padding: 20px;
1349
- border-radius: 15px;
1350
- width: 90%;
1351
- max-width: 700px;
1352
- box-shadow: 0 10px 30px rgba(0,0,0,0.2);
1353
- animation: slideIn 0.3s ease-out;
1354
- }
1355
- body.dark-mode .modal-content {
1356
- background: #2a2a3e;
1357
- color: #e0e0e0;
1358
- }
1359
- @keyframes slideIn {
1360
- from { transform: translateY(-50px); opacity: 0; }
1361
- to { transform: translateY(0); opacity: 1; }
1362
- }
1363
- .close {
1364
- float: right;
1365
- font-size: 1.5rem;
1366
- color: #666;
1367
- cursor: pointer;
1368
- transition: color 0.3s;
1369
- }
1370
- .close:hover {
1371
- color: #333;
1372
- }
1373
- body.dark-mode .close {
1374
- color: #bbb;
1375
- }
1376
- body.dark-mode .close:hover {
1377
- color: #fff;
1378
- }
1379
- .cart-item {
1380
- display: flex;
1381
- justify-content: space-between;
1382
- align-items: center;
1383
- padding: 15px 0;
1384
- border-bottom: 1px solid rgba(0, 0, 0, 0.1);
1385
- }
1386
- body.dark-mode .cart-item {
1387
- border-bottom: 1px solid rgba(255, 255, 255, 0.1);
1388
- }
1389
- .cart-item img {
1390
- width: 50px;
1391
- height: 50px;
1392
- object-fit: contain;
1393
- border-radius: 8px;
1394
- margin-right: 15px;
1395
- }
1396
- .quantity-input, .color-select {
1397
- width: 100%;
1398
- max-width: 150px;
1399
- padding: 8px;
1400
- border: 1px solid rgba(0, 0, 0, 0.1);
1401
- border-radius: 8px;
1402
- font-size: 1rem;
1403
- margin: 5px 0;
1404
- }
1405
- .clear-cart {
1406
- background-color: #e63946;
1407
- }
1408
- .clear-cart:hover {
1409
- background-color: #d62828;
1410
- box-shadow: 0 4px 15px rgba(230, 57, 70, 0.4);
1411
- }
1412
- .order-button {
1413
- background-color: #2ecc71;
1414
- }
1415
- .order-button:hover {
1416
- background-color: #27ae60;
1417
- box-shadow: 0 4px 15px rgba(46, 204, 113, 0.4);
1418
- }
1419
- .navbar {
1420
- position: fixed;
1421
- bottom: 0;
1422
- left: 0;
1423
- width: 100%;
1424
- background-color: #fff;
1425
- box-shadow: 0 -2px 10px rgba(0,0,0,0.1);
1426
- display: flex;
1427
- justify-content: space-around;
1428
- padding: 10px 0;
1429
- z-index: 1000;
1430
- }
1431
- body.dark-mode .navbar {
1432
- background-color: #2a2a3e;
1433
- }
1434
- .navbar a {
1435
- text-align: center;
1436
- color: #666;
1437
- text-decoration: none;
1438
- font-size: 0.9rem;
1439
- transition: color 0.3s ease;
1440
- }
1441
- .navbar a.active {
1442
- color: #526df2;
1443
- }
1444
- .navbar a i {
1445
- display: block;
1446
- font-size: 1.5rem;
1447
- margin-bottom: 5px;
1448
- }
1449
- body.dark-mode .navbar a {
1450
- color: #bbb;
1451
- }
1452
- .navbar a:hover {
1453
- color: #526df2;
1454
- }
1455
- .wholesale-badge {
1456
- position: absolute;
1457
- top: 10px;
1458
- right: 10px;
1459
- background-color: #526df2;
1460
- color: white;
1461
- padding: 5px 10px;
1462
- border-radius: 15px;
1463
- font-size: 0.75rem;
1464
- font-weight: 500;
1465
- }
1466
- .discount-badge {
1467
- position: absolute;
1468
- top: 40px;
1469
- right: 10px;
1470
- background-color: #2ecc71;
1471
- color: white;
1472
- padding: 5px 10px;
1473
- border-radius: 15px;
1474
- font-size: 0.75rem;
1475
- font-weight: 500;
1476
- }
1477
- </style>
1478
- </head>
1479
- <body>
1480
- <div class="container">
1481
- <div class="header">
1482
- <h1>{{ category }}</h1>
1483
- <button class="theme-toggle" onclick="toggleTheme()">
1484
- <i class="fas fa-moon"></i>
1485
- </button>
1486
- </div>
1487
- <div class="products-grid" id="products-grid">
1488
- {% for product in products %}
1489
- <div class="product"
1490
- onclick="openModal({{ loop.index0 }})"
1491
- data-name="{{ product['name']|lower }}"
1492
- data-description="{{ product['description']|lower }}"
1493
- data-category="{{ product.get('category', 'Без категории') }}">
1494
- <button class="favorite-button" onclick="event.stopPropagation(); toggleFavorite({{ loop.index0 }})">
1495
- <i class="fas fa-heart"></i>
1496
- </button>
1497
- {% if product.get('photos') and product['photos']|length > 0 %}
1498
- <div class="product-image">
1499
- <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product['photos'][0] }}"
1500
- alt="{{ product['name'] }}"
1501
- loading="lazy">
1502
- </div>
1503
- {% endif %}
1504
- {% if product.get('wholesale_price') and product.get('min_wholesale') %}
1505
- <span class="wholesale-badge">Опт от {{ product['min_wholesale'] }}</span>
1506
- {% endif %}
1507
- {% if product.get('discount') %}
1508
- <span class="discount-badge">Скидка {{ product['discount'] }}%</span>
1509
- {% endif %}
1510
- <h2>{{ product['name'] }}</h2>
1511
- <div class="product-price">
1512
- {% if product.get('discount') %}
1513
- <span style="text-decoration: line-through; color: #666;">{{ product['price'] }} с</span>
1514
- <span>{{ (product['price'] * (1 - product['discount'] / 100))|round(2) }} с</span>
1515
- <span class="discount">Скидка: {{ product['discount'] }}%</span>
1516
- {% else %}
1517
- <span>{{ product['price'] }} с</span>
1518
- {% endif %}
1519
- {% if product.get('wholesale_price') and product.get('min_wholesale') %}
1520
- <span class="wholesale">Опт: {{ product['wholesale_price'] }} с</span>
1521
- {% endif %}
1522
- </div>
1523
- <p class="product-description">{{ product['description'][:50] }}{% if product['description']|length > 50 %}...{% endif %}</p>
1524
- <button class="product-button add-to-cart" onclick="event.stopPropagation(); openQuantityModal({{ loop.index0 }})">В корзину</button>
1525
- </div>
1526
- {% endfor %}
1527
- </div>
1528
- </div>
1529
-
1530
- <!-- Product Modal -->
1531
- <div id="productModal" class="modal">
1532
- <div class="modal-content">
1533
- <span class="close" onclick="closeModal('productModal')">×</span>
1534
- <div id="modalContent"></div>
1535
- </div>
1536
- </div>
1537
-
1538
- <!-- Quantity and Color Modal -->
1539
- <div id="quantityModal" class="modal">
1540
- <div class="modal-content">
1541
- <span class="close" onclick="closeModal('quantityModal')">×</span>
1542
- <h2>Укажите количество и цвет</h2>
1543
- <input type="number" id="quantityInput" class="quantity-input" min="1" value="1">
1544
- <select id="colorSelect" class="color-select"></select>
1545
- <button class="product-button" onclick="confirmAddToCart()">Добавить</button>
1546
- </div>
1547
- </div>
1548
-
1549
- <!-- Cart Modal -->
1550
- <div id="cartModal" class="modal">
1551
- <div class="modal-content">
1552
- <span class="close" onclick="closeModal('cartModal')">×</span>
1553
- <h2>Корзина</h2>
1554
- <div id="cartContent"></div>
1555
- <div style="margin-top: 20px; text-align: right;">
1556
- <strong>Итого: <span id="cartTotal">0</span> с</strong>
1557
- <button class="product-button clear-cart" onclick="clearCart()">Очистить</button>
1558
- <button class="product-button order-button" onclick="orderViaWhatsApp()">Заказать</button>
1559
- </div>
1560
- </div>
1561
- </div>
1562
-
1563
- <!-- Address Modal -->
1564
- <div id="addressModal" class="modal">
1565
- <div class="modal-content">
1566
- <span class="close" onclick="closeModal('addressModal')">×</span>
1567
- <h2>Наш адрес</h2>
1568
- <p>Рынок Дордой, Джунхай, 5 проход 505-506 контейнеры</p>
1569
- </div>
1570
- </div>
1571
-
1572
- <button id="cart-button" onclick="openCartModal()">🛒</button>
1573
-
1574
- <div class="navbar">
1575
- <a href="/">
1576
- <i class="fas fa-home"></i>
1577
- Главная
1578
- </a>
1579
- <a href="/categories" class="active">
1580
- <i class="fas fa-list"></i>
1581
- Каталог
1582
- </a>
1583
- <a href="/favorites">
1584
- <i class="fas fa-heart"></i>
1585
- Избранное
1586
- </a>
1587
- <a href="/discounts">
1588
- <i class="fas fa-tag"></i>
1589
- Скидки
1590
- </a>
1591
- <a href="#" onclick="openAddressModal()">
1592
- <i class="fas fa-map-marker-alt"></i>
1593
- Адрес
1594
- </a>
1595
- <a href="https://api.whatsapp.com/send?phone=996500654659">
1596
- <i class="fab fa-whatsapp"></i>
1597
- WhatsApp
1598
- </a>
1599
- </div>
1600
-
1601
- <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
1602
- <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.5.3/dist/umd/popper.min.js"></script>
1603
- <script src="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.js"></script>
1604
- <script>
1605
- const products = {{ products|tojson }};
1606
- let selectedProductIndex = null;
1607
-
1608
- function toggleTheme() {
1609
- document.body.classList.toggle('dark-mode');
1610
- const icon = document.querySelector('.theme-toggle i');
1611
- icon.classList.toggle('fa-moon');
1612
- icon.classList.toggle('fa-sun');
1613
- localStorage.setItem('theme', document.body.classList.contains('dark-mode') ? 'dark' : 'light');
1614
- }
1615
-
1616
- if (localStorage.getItem('theme') === 'dark') {
1617
- document.body.classList.add('dark-mode');
1618
- document.querySelector('.theme-toggle i').classList.replace('fa-moon', 'fa-sun');
1619
- }
1620
-
1621
- function openModal(index) {
1622
- loadProductDetails(index);
1623
- document.getElementById('productModal').style.display = "block";
1624
- }
1625
-
1626
- function closeModal(modalId) {
1627
- document.getElementById(modalId).style.display = "none";
1628
- }
1629
-
1630
- function loadProductDetails(index) {
1631
- fetch('/product/' + index)
1632
- .then(response => response.text())
1633
- .then(data => {
1634
- document.getElementById('modalContent').innerHTML = data;
1635
- initializeSwiper();
1636
- })
1637
- .catch(error => console.error('Ошибка:', error));
1638
- }
1639
-
1640
- function initializeSwiper() {
1641
- new Swiper('.swiper-container', {
1642
- slidesPerView: 1,
1643
- spaceBetween: 20,
1644
- loop: true,
1645
- grabCursor: true,
1646
- pagination: { el: '.swiper-pagination', clickable: true },
1647
- navigation: { nextEl: '.swiper-button-next', prevEl: '.swiper-button-prev' },
1648
- zoom: { maxRatio: 3 }
1649
- });
1650
- }
1651
-
1652
- function openQuantityModal(index) {
1653
- selectedProductIndex = index;
1654
- const product = products[index];
1655
- const colorSelect = document.getElementById('colorSelect');
1656
- colorSelect.innerHTML = '';
1657
- if (product.colors && product.colors.length > 0) {
1658
- product.colors.forEach(color => {
1659
- const option = document.createElement('option');
1660
- option.value = color;
1661
- option.text = color;
1662
- colorSelect.appendChild(option);
1663
- });
1664
- } else {
1665
- const option = document.createElement('option');
1666
- option.value = 'Нет цвета';
1667
- option.text = 'Нет цвета';
1668
- colorSelect.appendChild(option);
1669
- }
1670
- document.getElementById('quantityModal').style.display = 'block';
1671
- document.getElementById('quantityInput').value = 1;
1672
- }
1673
-
1674
- function confirmAddToCart() {
1675
- if (selectedProductIndex === null) return;
1676
- const quantity = parseInt(document.getElementById('quantityInput').value) || 1;
1677
- const color = document.getElementById('colorSelect').value;
1678
- if (quantity <= 0) {
1679
- alert("Укажите количество больше 0");
1680
- return;
1681
- }
1682
- let cart = JSON.parse(localStorage.getItem('cart') || '[]');
1683
- const product = products[selectedProductIndex];
1684
- const cartItemId = `${product.name}-${color}`;
1685
- const existingItem = cart.find(item => item.id === cartItemId);
1686
-
1687
- let priceToUse = product.price;
1688
- if (product.discount) {
1689
- priceToUse = product.price * (1 - product.discount / 100);
1690
- }
1691
- if (product.min_wholesale && quantity >= product.min_wholesale) {
1692
- priceToUse = product.wholesale_price;
1693
- }
1694
-
1695
- if (existingItem) {
1696
- existingItem.quantity += quantity;
1697
- existingItem.price = (existingItem.quantity >= product.min_wholesale) ? product.wholesale_price : (product.discount ? product.price * (1 - product.discount / 100) : product.price);
1698
- } else {
1699
- cart.push({
1700
- id: cartItemId,
1701
- name: product.name,
1702
- price: priceToUse,
1703
- retail_price: product.price,
1704
- wholesale_price: product.wholesale_price,
1705
- min_wholesale: product.min_wholesale,
1706
- discount: product.discount,
1707
- photo: product.photos && product.photos.length > 0 ? product.photos[0] : '',
1708
- quantity: quantity,
1709
- color: color
1710
- });
1711
- }
1712
 
1713
- localStorage.setItem('cart', JSON.stringify(cart));
1714
- closeModal('quantityModal');
1715
- updateCartButton();
 
 
 
 
 
 
 
 
 
1716
  }
1717
 
1718
  function updateCartButton() {
@@ -1747,6 +1159,10 @@ def category_products(category):
1747
  document.getElementById('cartModal').style.display = 'block';
1748
  }
1749
 
 
 
 
 
1750
  function orderViaWhatsApp() {
1751
  const cart = JSON.parse(localStorage.getItem('cart') || '[]');
1752
  if (cart.length === 0) {
@@ -1770,59 +1186,29 @@ def category_products(category):
1770
  updateCartButton();
1771
  }
1772
 
1773
- function toggleFavorite(index) {
1774
- let favorites = JSON.parse(localStorage.getItem('favorites') || '[]');
1775
- const productId = index.toString();
1776
- const favoriteButton = document.querySelector(`.favorite-button[onclick="event.stopPropagation(); toggleFavorite(${index})"]`);
1777
- if (favorites.includes(productId)) {
1778
- favorites = favorites.filter(id => id !== productId);
1779
- favoriteButton.classList.remove('favorited');
1780
- } else {
1781
- favorites.push(productId);
1782
- favoriteButton.classList.add('favorited');
1783
- }
1784
- localStorage.setItem('favorites', JSON.stringify(favorites));
1785
- }
1786
-
1787
- function loadFavorites() {
1788
- const favorites = JSON.parse(localStorage.getItem('favorites') || '[]');
1789
- document.querySelectorAll('.favorite-button').forEach(button => {
1790
- const index = button.getAttribute('onclick').match(/\d+/)[0];
1791
- if (favorites.includes(index)) {
1792
- button.classList.add('favorited');
1793
- }
1794
- });
1795
- }
1796
-
1797
- function openAddressModal() {
1798
- document.getElementById('addressModal').style.display = 'block';
1799
- }
1800
-
1801
  window.onclick = function(event) {
1802
  if (event.target.className === 'modal') event.target.style.display = "none";
1803
  }
1804
 
1805
  updateCartButton();
1806
- loadFavorites();
1807
  </script>
1808
  </body>
1809
  </html>
1810
  '''
1811
- return render_template_string(category_html, products=products, category=category, repo_id=REPO_ID)
1812
 
1813
- @app.route('/favorites')
1814
- def favorites_page():
1815
  data = load_data()
1816
- products = data['products']
1817
- favorites = request.args.get('favorites', '[]')
1818
 
1819
- favorites_html = '''
1820
  <!DOCTYPE html>
1821
  <html lang="ru">
1822
  <head>
1823
  <meta charset="UTF-8">
1824
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
1825
- <title>Избранное</title>
1826
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
1827
  <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
1828
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.css">
@@ -2193,6 +1579,7 @@ def favorites_page():
2193
  </button>
2194
  </div>
2195
  <div class="products-grid" id="products-grid">
 
2196
  </div>
2197
  </div>
2198
 
@@ -2229,15 +1616,6 @@ def favorites_page():
2229
  </div>
2230
  </div>
2231
 
2232
- <!-- Address Modal -->
2233
- <div id="addressModal" class="modal">
2234
- <div class="modal-content">
2235
- <span class="close" onclick="closeModal('addressModal')">×</span>
2236
- <h2>Наш адрес</h2>
2237
- <p>Рынок Дордой, Джунхай, 5 проход 505-506 контейнеры</p>
2238
- </div>
2239
- </div>
2240
-
2241
  <button id="cart-button" onclick="openCartModal()">🛒</button>
2242
 
2243
  <div class="navbar">
@@ -2257,10 +1635,6 @@ def favorites_page():
2257
  <i class="fas fa-tag"></i>
2258
  Скидки
2259
  </a>
2260
- <a href="#" onclick="openAddressModal()">
2261
- <i class="fas fa-map-marker-alt"></i>
2262
- Адрес
2263
- </a>
2264
  <a href="https://api.whatsapp.com/send?phone=996500654659">
2265
  <i class="fab fa-whatsapp"></i>
2266
  WhatsApp
@@ -2287,7 +1661,7 @@ def favorites_page():
2287
  document.querySelector('.theme-toggle i').classList.replace('fa-moon', 'fa-sun');
2288
  }
2289
 
2290
- function loadFavoritesPage() {
2291
  const favorites = JSON.parse(localStorage.getItem('favorites') || '[]');
2292
  const productsGrid = document.getElementById('products-grid');
2293
  productsGrid.innerHTML = '';
@@ -2300,30 +1674,43 @@ def favorites_page():
2300
  favorites.forEach(index => {
2301
  const product = products[index];
2302
  if (!product) return;
 
2303
  const productElement = document.createElement('div');
2304
  productElement.className = 'product';
2305
  productElement.setAttribute('onclick', `openModal(${index})`);
2306
  productElement.setAttribute('data-name', product.name.toLowerCase());
2307
  productElement.setAttribute('data-description', product.description.toLowerCase());
2308
  productElement.setAttribute('data-category', product.category || 'Без категории');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2309
  productElement.innerHTML = `
2310
  <button class="favorite-button favorited" onclick="event.stopPropagation(); toggleFavorite(${index})">
2311
  <i class="fas fa-heart"></i>
2312
  </button>
2313
  ${product.photos && product.photos.length > 0 ? `
2314
  <div class="product-image">
2315
- <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/${product.photos[0]}" alt="${product.name}" loading="lazy">
 
 
2316
  </div>` : ''}
2317
  ${product.wholesale_price && product.min_wholesale ? `<span class="wholesale-badge">Опт от ${product.min_wholesale}</span>` : ''}
2318
  ${product.discount ? `<span class="discount-badge">Скидка ${product.discount}%</span>` : ''}
2319
  <h2>${product.name}</h2>
2320
- <div class="product-price">
2321
- ${product.discount ? `
2322
- <span style="text-decoration: line-through; color: #666;">${product.price} с</span>
2323
- <span>${(product.price * (1 - product.discount / 100)).toFixed(2)} с</span>
2324
- <span class="discount">Скидка: ${product.discount}%</span>` : `<span>${product.price} с</span>`}
2325
- ${product.wholesale_price && product.min_wholesale ? `<span class="wholesale">Опт: ${product.wholesale_price} с</span>` : ''}
2326
- </div>
2327
  <p class="product-description">${product.description.slice(0, 50)}${product.description.length > 50 ? '...' : ''}</p>
2328
  <button class="product-button add-to-cart" onclick="event.stopPropagation(); openQuantityModal(${index})">В корзину</button>
2329
  `;
@@ -2486,20 +1873,13 @@ def favorites_page():
2486
  function toggleFavorite(index) {
2487
  let favorites = JSON.parse(localStorage.getItem('favorites') || '[]');
2488
  const productId = index.toString();
2489
- const favoriteButton = document.querySelector(`.favorite-button[onclick="event.stopPropagation(); toggleFavorite(${index})"]`);
2490
  if (favorites.includes(productId)) {
2491
  favorites = favorites.filter(id => id !== productId);
2492
- favoriteButton.classList.remove('favorited');
2493
  } else {
2494
  favorites.push(productId);
2495
- favoriteButton.classList.add('favorited');
2496
  }
2497
  localStorage.setItem('favorites', JSON.stringify(favorites));
2498
- loadFavoritesPage();
2499
- }
2500
-
2501
- function openAddressModal() {
2502
- document.getElementById('addressModal').style.display = 'block';
2503
  }
2504
 
2505
  window.onclick = function(event) {
@@ -2507,7 +1887,7 @@ def favorites_page():
2507
  }
2508
 
2509
  updateCartButton();
2510
- loadFavoritesPage();
2511
  </script>
2512
  </body>
2513
  </html>
@@ -2518,7 +1898,6 @@ def favorites_page():
2518
  def discounts_page():
2519
  data = load_data()
2520
  products = [p for p in data['products'] if p.get('discount')]
2521
- categories = data['categories']
2522
 
2523
  discounts_html = '''
2524
  <!DOCTYPE html>
@@ -2972,15 +2351,6 @@ def discounts_page():
2972
  </div>
2973
  </div>
2974
 
2975
- <!-- Address Modal -->
2976
- <div id="addressModal" class="modal">
2977
- <div class="modal-content">
2978
- <span class="close" onclick="closeModal('addressModal')">×</span>
2979
- <h2>Наш адрес</h2>
2980
- <p>Рынок Дордой, Джунхай, 5 проход 505-506 контейнеры</p>
2981
- </div>
2982
- </div>
2983
-
2984
  <button id="cart-button" onclick="openCartModal()">🛒</button>
2985
 
2986
  <div class="navbar">
@@ -3000,10 +2370,6 @@ def discounts_page():
3000
  <i class="fas fa-tag"></i>
3001
  Скидки
3002
  </a>
3003
- <a href="#" onclick="openAddressModal()">
3004
- <i class="fas fa-map-marker-alt"></i>
3005
- Адрес
3006
- </a>
3007
  <a href="https://api.whatsapp.com/send?phone=996500654659">
3008
  <i class="fab fa-whatsapp"></i>
3009
  WhatsApp
@@ -3194,6 +2560,7 @@ def discounts_page():
3194
  favoriteButton.classList.add('favorited');
3195
  }
3196
  localStorage.setItem('favorites', JSON.stringify(favorites));
 
3197
  }
3198
 
3199
  function loadFavorites() {
@@ -3206,10 +2573,6 @@ def discounts_page():
3206
  });
3207
  }
3208
 
3209
- function openAddressModal() {
3210
- document.getElementById('addressModal').style.display = 'block';
3211
- }
3212
-
3213
  window.onclick = function(event) {
3214
  if (event.target.className === 'modal') event.target.style.display = "none";
3215
  }
@@ -3220,7 +2583,7 @@ def discounts_page():
3220
  </body>
3221
  </html>
3222
  '''
3223
- return render_template_string(discounts_html, products=products, categories=categories, repo_id=REPO_ID)
3224
 
3225
  @app.route('/product/<int:index>')
3226
  def product_details(index):
@@ -3231,7 +2594,7 @@ def product_details(index):
3231
  <style>
3232
  .swiper-container {
3233
  width: 100%;
3234
- max-width: 400px;
3235
  margin: 20px auto;
3236
  }
3237
  .swiper-slide {
@@ -3283,25 +2646,24 @@ def product_details(index):
3283
  .product-details p {
3284
  font-size: 0.95rem;
3285
  color: #666;
3286
- margin-bottom: 10px;
3287
  }
3288
  body.dark-mode .product-details p {
3289
  color: #bbb;
3290
  }
3291
- .product-details .colors {
3292
  margin: 10px 0;
3293
  }
3294
- .product-details .colors span {
3295
  display: inline-block;
3296
  margin-right: 10px;
3297
  padding: 5px 10px;
3298
- border-radius: 15px;
3299
- background: #f0f0f0;
3300
  font-size: 0.9rem;
3301
  }
3302
- body.dark-mode .product-details .colors span {
3303
- background: #444;
3304
- color: #e0e0e0;
3305
  }
3306
  </style>
3307
  <div class="product-details">
@@ -3310,7 +2672,9 @@ def product_details(index):
3310
  <div class="swiper-wrapper">
3311
  {% for photo in product.get('photos', []) %}
3312
  <div class="swiper-slide">
3313
- <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo }}" alt="{{ product['name'] }}">
 
 
3314
  </div>
3315
  {% endfor %}
3316
  </div>
@@ -3330,9 +2694,8 @@ def product_details(index):
3330
  <span class="wholesale">Опт: {{ product['wholesale_price'] }} с (от {{ product['min_wholesale'] }})</span>
3331
  {% endif %}
3332
  </div>
3333
- <p><strong>Описание:</strong> {{ product['description'] }}</p>
3334
- <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
3335
- {% if product.get('colors') %}
3336
  <div class="colors">
3337
  <strong>Цвета:</strong>
3338
  {% for color in product['colors'] %}
@@ -3344,427 +2707,10 @@ def product_details(index):
3344
  '''
3345
  return render_template_string(product_html, product=product, repo_id=REPO_ID)
3346
 
3347
- @app.route('/admin', methods=['GET', 'POST'])
3348
- def admin():
3349
- data = load_data()
3350
- products = data['products']
3351
- categories = data['categories']
3352
-
3353
- if request.method == 'POST':
3354
- action = request.form.get('action')
3355
-
3356
- if action == 'add':
3357
- photos = request.files.getlist('photos')
3358
- photo_filenames = []
3359
- for photo in photos:
3360
- if photo and photo.filename:
3361
- filename = secure_filename(photo.filename)
3362
- api = HfApi()
3363
- api.upload_file(
3364
- path_or_fileobj=photo,
3365
- path_in_repo=f"photos/{filename}",
3366
- repo_id=REPO_ID,
3367
- repo_type="dataset",
3368
- token=HF_TOKEN_WRITE
3369
- )
3370
- photo_filenames.append(filename)
3371
-
3372
- colors = request.form.getlist('colors')
3373
- colors = [color.strip() for color in colors if color.strip()]
3374
-
3375
- new_product = {
3376
- 'name': request.form['name'],
3377
- 'price': float(request.form['price']),
3378
- 'wholesale_price': float(request.form['wholesale_price']) if request.form['wholesale_price'] else None,
3379
- 'min_wholesale': int(request.form['min_wholesale']) if request.form['min_wholesale'] else None,
3380
- 'description': request.form['description'],
3381
- 'category': request.form['category'],
3382
- 'colors': colors,
3383
- 'photos': photo_filenames,
3384
- 'discount': float(request.form['discount']) if request.form['discount'] else None
3385
- }
3386
- products.append(new_product)
3387
-
3388
- elif action == 'edit':
3389
- index = int(request.form['index'])
3390
- photos = request.files.getlist('photos')
3391
- photo_filenames = products[index].get('photos', [])
3392
- for photo in photos:
3393
- if photo and photo.filename:
3394
- filename = secure_filename(photo.filename)
3395
- api = HfApi()
3396
- api.upload_file(
3397
- path_or_fileobj=photo,
3398
- path_in_repo=f"photos/{filename}",
3399
- repo_id=REPO_ID,
3400
- repo_type="dataset",
3401
- token=HF_TOKEN_WRITE
3402
- )
3403
- photo_filenames.append(filename)
3404
-
3405
- colors = request.form.getlist('colors')
3406
- colors = [color.strip() for color in colors if color.strip()]
3407
-
3408
- products[index] = {
3409
- 'name': request.form['name'],
3410
- 'price': float(request.form['price']),
3411
- 'wholesale_price': float(request.form['wholesale_price']) if request.form['wholesale_price'] else None,
3412
- 'min_wholesale': int(request.form['min_wholesale']) if request.form['min_wholesale'] else None,
3413
- 'description': request.form['description'],
3414
- 'category': request.form['category'],
3415
- 'colors': colors,
3416
- 'photos': photo_filenames,
3417
- 'discount': float(request.form['discount']) if request.form['discount'] else None
3418
- }
3419
-
3420
- elif action == 'delete':
3421
- index = int(request.form['index'])
3422
- products.pop(index)
3423
-
3424
- elif action == 'add_category':
3425
- category = request.form['category_name'].strip()
3426
- if category and category not in categories:
3427
- categories.append(category)
3428
-
3429
- elif action == 'delete_category':
3430
- index = int(request.form['category_index'])
3431
- category_to_delete = categories[index]
3432
- categories.pop(index)
3433
- for product in products:
3434
- if product.get('category') == category_to_delete:
3435
- product['category'] = 'Без категории'
3436
-
3437
- save_data({'products': products, 'categories': categories})
3438
- return redirect(url_for('admin'))
3439
-
3440
- admin_html = '''
3441
- <!DOCTYPE html>
3442
- <html lang="ru">
3443
- <head>
3444
- <meta charset="UTF-8">
3445
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
3446
- <title>Админ-панель</title>
3447
- <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
3448
- <style>
3449
- * {
3450
- margin: 0;
3451
- padding: 0;
3452
- box-sizing: border-box;
3453
- }
3454
- body {
3455
- font-family: 'Roboto', sans-serif;
3456
- background: linear-gradient(135deg, #f5f7fa, #e4e7eb);
3457
- color: #333;
3458
- line-height: 1.6;
3459
- padding: 20px;
3460
- }
3461
- .container {
3462
- max-width: 1400px;
3463
- margin: 0 auto;
3464
- }
3465
- h1 {
3466
- font-size: 2rem;
3467
- font-weight: 700;
3468
- margin-bottom: 20px;
3469
- text-align: center;
3470
- background: linear-gradient(90deg, #526df2, #7a8ff5);
3471
- -webkit-background-clip: text;
3472
- -webkit-text-fill-color: transparent;
3473
- }
3474
- .form-container, .category-list, .product-list {
3475
- background: rgba(255, 255, 255, 0.9);
3476
- border-radius: 15px;
3477
- padding: 20px;
3478
- margin-bottom: 20px;
3479
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
3480
- }
3481
- .form-container label, .edit-form label {
3482
- display: block;
3483
- margin: 10px 0 5px;
3484
- font-weight: 500;
3485
- }
3486
- .form-container input, .form-container textarea, .form-container select,
3487
- .edit-form input, .edit-form textarea, .edit-form select {
3488
- width: 100%;
3489
- padding: 10px;
3490
- margin-bottom: 10px;
3491
- border: 1px solid rgba(0, 0, 0, 0.1);
3492
- border-radius: 8px;
3493
- font-size: 1rem;
3494
- }
3495
- .form-container textarea, .edit-form textarea {
3496
- resize: vertical;
3497
- }
3498
- .form-container button, .edit-form button {
3499
- padding: 10px 20px;
3500
- border: none;
3501
- border-radius: 25px;
3502
- background-color: #526df2;
3503
- color: white;
3504
- font-size: 1rem;
3505
- font-weight: 500;
3506
- cursor: pointer;
3507
- transition: all 0.3s ease;
3508
- margin: 5px 0;
3509
- }
3510
- .form-container button:hover, .edit-form button:hover {
3511
- background-color: #3e55d1;
3512
- box-shadow: 0 4px 15px rgba(62, 85, 209, 0.4);
3513
- }
3514
- .add-color-btn {
3515
- background-color: #2ecc71;
3516
- }
3517
- .add-color-btn:hover {
3518
- background-color: #27ae60;
3519
- box-shadow: 0 4px 15px rgba(46, 204, 113, 0.4);
3520
- }
3521
- .color-input-group {
3522
- display: flex;
3523
- align-items: center;
3524
- margin-bottom: 10px;
3525
- }
3526
- .color-input-group input {
3527
- flex: 1;
3528
- }
3529
- .category-list, .product-list {
3530
- display: grid;
3531
- gap: 20px;
3532
- }
3533
- .category-item, .product-item {
3534
- background: rgba(255, 255, 255, 0.95);
3535
- border: 1px solid rgba(0, 0, 0, 0.05);
3536
- border-radius: 10px;
3537
- padding: 15px;
3538
- box-shadow: 0 2px 10px rgba(0, 0, 0, 0.03);
3539
- }
3540
- .category-item h2, .product-item h2 {
3541
- font-size: 1.2rem;
3542
- font-weight: 500;
3543
- margin-bottom: 10px;
3544
- }
3545
- .category-item form, .product-item form {
3546
- display: inline-block;
3547
- }
3548
- .delete-button {
3549
- background-color: #e63946;
3550
- }
3551
- .delete-button:hover {
3552
- background-color: #d62828;
3553
- box-shadow: 0 4px 15px rgba(230, 57, 70, 0.4);
3554
- }
3555
- details {
3556
- margin: 10px 0;
3557
- }
3558
- summary {
3559
- cursor: pointer;
3560
- font-weight: 500;
3561
- color: #526df2;
3562
- margin-bottom: 10px;
3563
- }
3564
- .edit-form {
3565
- margin-top: 10px;
3566
- }
3567
- .search-container {
3568
- margin: 20px 0;
3569
- text-align: center;
3570
- }
3571
- #search-input {
3572
- width: 90%;
3573
- max-width: 600px;
3574
- padding: 12px 18px;
3575
- font-size: 1rem;
3576
- border: 1px solid rgba(0, 0, 0, 0.1);
3577
- border-radius: 25px;
3578
- outline: none;
3579
- box-shadow: 0 2px 5px rgba(0,0,0,0.05);
3580
- transition: all 0.3s ease;
3581
- }
3582
- #search-input:focus {
3583
- border-color: #526df2;
3584
- box-shadow: 0 4px 15px rgba(82, 109, 242, 0.2);
3585
- }
3586
- </style>
3587
- </head>
3588
- <body>
3589
- <div class="container">
3590
- <h1>Админ-панель</h1>
3591
-
3592
- <div class="form-container">
3593
- <h1>Добавить товар</h1>
3594
- <form method="POST" enctype="multipart/form-data">
3595
- <input type="hidden" name="action" value="add">
3596
- <label>Название:</label>
3597
- <input type="text" name="name" required>
3598
- <label>Цена (розница):</label>
3599
- <input type="number" name="price" step="0.01" required>
3600
- <label>Цена (опт):</label>
3601
- <input type="number" name="wholesale_price" step="0.01">
3602
- <label>Минимальное количество для опта:</label>
3603
- <input type="number" name="min_wholesale" min="1">
3604
- <label>Скидка (%):</label>
3605
- <input type="number" name="discount" min="0" max="100" step="0.1">
3606
- <label>Описание:</label>
3607
- <textarea name="description" rows="4" required></textarea>
3608
- <label>Категория:</label>
3609
- <select name="category">
3610
- <option value="Без категории">Без категории</option>
3611
- {% for category in categories %}
3612
- <option value="{{ category }}">{{ category }}</option>
3613
- {% endfor %}
3614
- </select>
3615
- <label>Фотографии (до 10):</label>
3616
- <input type="file" name="photos" accept="image/*" multiple>
3617
- <label>Цвета:</label>
3618
- <div id="color-inputs">
3619
- <div class="color-input-group">
3620
- <input type="text" name="colors" placeholder="Например: Красный">
3621
- </div>
3622
- </div>
3623
- <button type="button" class="add-color-btn" onclick="addColorInput()">Добавить цвет</button>
3624
- <button type="submit">Добавить товар</button>
3625
- </form>
3626
- </div>
3627
-
3628
- <div class="form-container">
3629
- <h1>Добавить категорию</h1>
3630
- <form method="POST">
3631
- <input type="hidden" name="action" value="add_category">
3632
- <label>Название категории:</label>
3633
- <input type="text" name="category_name" required>
3634
- <button type="submit">Добавить категорию</button>
3635
- </form>
3636
- </div>
3637
-
3638
- <div class="category-list">
3639
- {% for category in categories %}
3640
- <div class="category-item">
3641
- <h2>{{ category }}</h2>
3642
- <form method="POST">
3643
- <input type="hidden" name="action" value="delete_category">
3644
- <input type="hidden" name="category_index" value="{{ loop.index0 }}">
3645
- <button type="submit" class="delete-button">Удалить категорию</button>
3646
- </form>
3647
- </div>
3648
- {% endfor %}
3649
- </div>
3650
-
3651
- <h1>Список товаров</h1>
3652
- <div class="search-container">
3653
- <input type="text" id="search-input" placeholder="Поиск товаров...">
3654
- </div>
3655
- <div class="product-list" id="product-list">
3656
- {% for product in products %}
3657
- <div class="product-item"
3658
- data-name="{{ product['name']|lower }}"
3659
- data-description="{{ product['description']|lower }}"
3660
- data-category="{{ product.get('category', 'Без категории') }}">
3661
- <h2>{{ product['name'] }}</h2>
3662
- <p>Цена (розница): {{ product['price'] }} с</p>
3663
- {% if product.get('wholesale_price') and product.get('min_wholesale') %}
3664
- <p>Цена (опт): {{ product['wholesale_price'] }} с (от {{ product['min_wholesale'] }})</p>
3665
- {% endif %}
3666
- {% if product.get('discount') %}
3667
- <p>Скидка: {{ product['discount'] }}%</p>
3668
- {% endif %}
3669
- <p>Описание: {{ product['description'] }}</p>
3670
- <p>Категория: {{ product.get('category', 'Без категории') }}</p>
3671
- <p>Цвета: {{ product.get('colors', ['Нет цветов'])|join(', ') }}</p>
3672
- {% if product.get('photos') %}
3673
- <p>Фотографии:</p>
3674
- {% for photo in product['photos'] %}
3675
- <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo }}" alt="{{ product['name'] }}" style="max-width: 100px; margin: 5px;">
3676
- {% endfor %}
3677
- {% endif %}
3678
- <details>
3679
- <summary>Редактировать</summary>
3680
- <form method="POST" enctype="multipart/form-data" class="edit-form">
3681
- <input type="hidden" name="action" value="edit">
3682
- <input type="hidden" name="index" value="{{ loop.index0 }}">
3683
- <label>Название:</label>
3684
- <input type="text" name="name" value="{{ product['name'] }}" required>
3685
- <label>Цена (розница):</label>
3686
- <input type="number" name="price" step="0.01" value="{{ product['price'] }}" required>
3687
- <label>Цена (опт):</label>
3688
- <input type="number" name="wholesale_price" step="0.01" value="{{ product.get('wholesale_price', '') }}">
3689
- <label>Минимальное количество для опта:</label>
3690
- <input type="number" name="min_wholesale" min="1" value="{{ product.get('min_wholesale', '') }}">
3691
- <label>Скидка (%):</label>
3692
- <input type="number" name="discount" min="0" max="100" step="0.1" value="{{ product.get('discount', '') }}">
3693
- <label>Описание:</label>
3694
- <textarea name="description" rows="4" required>{{ product['description'] }}</textarea>
3695
- <label>Категория:</label>
3696
- <select name="category">
3697
- <option value="Без категории" {% if product.get('category', 'Без категории') == 'Без категории' %}selected{% endif %}>Без категории</option>
3698
- {% for category in categories %}
3699
- <option value="{{ category }}" {% if product.get('category') == category %}selected{% endif %}>{{ category }}</option>
3700
- {% endfor %}
3701
- </select>
3702
- <label>Фотографии (до 10):</label>
3703
- <input type="file" name="photos" accept="image/*" multiple>
3704
- <label>Цвета:</label>
3705
- <div id="edit-color-inputs-{{ loop.index0 }}">
3706
- {% for color in product.get('colors', []) %}
3707
- <div class="color-input-group">
3708
- <input type="text" name="colors" value="{{ color }}">
3709
- </div>
3710
- {% endfor %}
3711
- </div>
3712
- <button type="button" class="add-color-btn" onclick="addColorInput('edit-color-inputs-{{ loop.index0 }}')">Добавить цвет</button>
3713
- <button type="submit">Сохранить</button>
3714
- </form>
3715
- </details>
3716
- <form method="POST">
3717
- <input type="hidden" name="action" value="delete">
3718
- <input type="hidden" name="index" value="{{ loop.index0 }}">
3719
- <button type="submit" class="delete-button">Удалить</button>
3720
- </form>
3721
- </div>
3722
- {% endfor %}
3723
- </div>
3724
- </div>
3725
- <script>
3726
- function addColorInput(containerId = 'color-inputs') {
3727
- const container = document.getElementById(containerId);
3728
- const newInput = document.createElement('div');
3729
- newInput.className = 'color-input-group';
3730
- newInput.innerHTML = '<input type="text" name="colors" placeholder="Например: Красный">';
3731
- container.appendChild(newInput);
3732
- }
3733
-
3734
- document.getElementById('search-input').addEventListener('input', filterProducts);
3735
-
3736
- function filterProducts() {
3737
- const searchTerm = document.getElementById('search-input').value.toLowerCase();
3738
- document.querySelectorAll('.product-item').forEach(product => {
3739
- const name = product.getAttribute('data-name');
3740
- const description = product.getAttribute('data-description');
3741
- const category = product.getAttribute('data-category');
3742
- const matchesSearch = name.includes(searchTerm) || description.includes(searchTerm) || category.includes(searchTerm);
3743
- product.style.display = matchesSearch ? 'block' : 'none';
3744
- });
3745
- }
3746
- </script>
3747
- </body>
3748
- </html>
3749
- '''
3750
- return render_template_string(admin_html, products=products, categories=categories, repo_id=REPO_ID)
3751
-
3752
- @app.route('/backup', methods=['POST'])
3753
- def backup():
3754
- upload_db_to_hf()
3755
- return "Резервная копия создана.", 200
3756
-
3757
- @app.route('/download', methods=['GET'])
3758
- def download():
3759
- download_db_from_hf()
3760
- return "База данных скачана.", 200
3761
 
3762
  if __name__ == '__main__':
3763
- backup_thread = threading.Thread(target=periodic_backup, daemon=True)
3764
- backup_thread.start()
3765
- try:
3766
- load_data()
3767
- except Exception as e:
3768
- logging.error(f"Не удалось загрузить базу данных: {e}")
3769
  app.run(debug=True, host='0.0.0.0', port=7860)
3770
 
 
 
1
  import json
 
2
  import logging
3
  import threading
4
  import time
5
  from datetime import datetime
6
+ from flask import Flask, request, redirect, url_for, render_template_string
7
+ from huggingface_hub import HfApi, login
8
  from werkzeug.utils import secure_filename
9
 
10
  app = Flask(__name__)
 
11
 
12
+ # Настройки логирования
13
+ logging.basicConfig(level=logging.INFO)
 
 
14
 
15
+ # Конфигурация Hugging Face
16
+ HF_TOKEN_READ = "hf_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # Ваш токен для чтения
17
+ HF_TOKEN_WRITE = "hf_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # Ваш токен для записи
18
+ REPO_ID = "your_username/your_dataset" # Замените на ваш репозиторий
19
+ DB_FILE = "db.json"
20
 
21
+ # Авторизация в Hugging Face
22
+ login(token=HF_TOKEN_READ)
23
+
24
+ # Загрузка данных
25
  def load_data():
26
  try:
27
+ api = HfApi()
28
+ db_content = api.hf_hub_download(repo_id=REPO_ID, filename=DB_FILE, repo_type="dataset", token=HF_TOKEN_READ)
29
+ with open(db_content, 'r', encoding='utf-8') as f:
30
+ return json.load(f)
 
 
 
 
 
 
 
 
 
 
 
 
31
  except Exception as e:
32
+ logging.error(f"Ошибка загрузки данных: {e}")
33
+ return {"products": [], "categories": []}
34
 
35
+ # Сохранение данных
36
  def save_data(data):
37
+ with open(DB_FILE, 'w', encoding='utf-8') as f:
38
+ json.dump(data, f, ensure_ascii=False, indent=4)
39
+ api = HfApi()
40
+ api.upload_file(
41
+ path_or_fileobj=DB_FILE,
42
+ path_in_repo=DB_FILE,
43
+ repo_id=REPO_ID,
44
+ repo_type="dataset",
45
+ token=HF_TOKEN_WRITE
46
+ )
47
+
48
+ # Периодический бэкап
49
+ def periodic_backup():
50
+ while True:
51
+ time.sleep(24 * 60 * 60) # Каждые 24 часа
52
  upload_db_to_hf()
 
 
 
53
 
54
  def upload_db_to_hf():
55
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
56
+ backup_file = f"backups/db_{timestamp}.json"
57
+ api = HfApi()
58
+ api.upload_file(
59
+ path_or_fileobj=DB_FILE,
60
+ path_in_repo=backup_file,
61
+ repo_id=REPO_ID,
62
+ repo_type="dataset",
63
+ token=HF_TOKEN_WRITE
64
+ )
65
+ logging.info(f"Бэкап создан: {backup_file}")
 
 
66
 
67
  def download_db_from_hf():
68
+ api = HfApi()
69
+ db_content = api.hf_hub_download(repo_id=REPO_ID, filename=DB_FILE, repo_type="dataset", token=HF_TOKEN_READ)
70
+ with open(DB_FILE, 'wb') as f:
71
+ with open(db_content, 'rb') as src:
72
+ f.write(src.read())
73
+ logging.info("База данных скачана.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
 
75
  @app.route('/')
76
+ def index():
77
  data = load_data()
78
  products = data['products']
 
79
 
80
+ index_html = '''
81
  <!DOCTYPE html>
82
  <html lang="ru">
83
  <head>
84
  <meta charset="UTF-8">
85
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
86
+ <title>Канцелярия оптом</title>
87
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
88
  <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
89
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.css">
 
139
  body.dark-mode .theme-toggle {
140
  color: #bbb;
141
  }
 
 
 
 
 
 
 
142
  .search-container {
143
  margin: 20px 0;
144
  text-align: center;
 
158
  border-color: #526df2;
159
  box-shadow: 0 4px 15px rgba(82, 109, 242, 0.2);
160
  }
161
+ body.dark-mode #search-input {
162
+ background: #2a2a3e;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
  color: #e0e0e0;
164
+ border-color: rgba(255, 255, 255, 0.1);
165
  }
166
  .products-grid {
167
  display: grid;
 
467
  font-size: 0.75rem;
468
  font-weight: 500;
469
  }
470
+ .contact-info {
471
+ text-align: center;
472
+ margin: 20px 0;
473
+ font-size: 1rem;
474
+ color: #666;
475
+ }
476
+ body.dark-mode .contact-info {
477
+ color: #bbb;
478
+ }
479
  </style>
480
  </head>
481
  <body>
482
  <div class="container">
483
  <div class="header">
484
+ <h1>Канцелярия оптом</h1>
485
  <button class="theme-toggle" onclick="toggleTheme()">
486
  <i class="fas fa-moon"></i>
487
  </button>
488
  </div>
489
+ <div class="contact-info">
490
+ <p>Рынок Дордой, Джунхай, 5 проход 505-506 контейнеры</p>
491
+ <p>График работы: с 9:00 до 16:00, без выходных</p>
 
 
492
  </div>
493
  <div class="search-container">
494
  <input type="text" id="search-input" placeholder="Поиск товаров...">
 
569
  </div>
570
  </div>
571
 
 
 
 
 
 
 
 
 
 
572
  <button id="cart-button" onclick="openCartModal()">🛒</button>
573
 
 
574
  <div class="navbar">
575
  <a href="/" class="active">
576
  <i class="fas fa-home"></i>
 
588
  <i class="fas fa-tag"></i>
589
  Скидки
590
  </a>
 
 
 
 
591
  <a href="https://api.whatsapp.com/send?phone=996500654659">
592
  <i class="fab fa-whatsapp"></i>
593
  WhatsApp
 
614
  document.querySelector('.theme-toggle i').classList.replace('fa-moon', 'fa-sun');
615
  }
616
 
617
+ function filterProducts() {
618
+ const searchTerm = document.getElementById('search-input').value.toLowerCase();
619
+ document.querySelectorAll('.product').forEach(product => {
620
+ const name = product.getAttribute('data-name');
621
+ const description = product.getAttribute('data-description');
622
+ const category = product.getAttribute('data-category');
623
+ const matchesSearch = name.includes(searchTerm) || description.includes(searchTerm) || category.includes(searchTerm);
624
+ product.style.display = matchesSearch ? 'block' : 'none';
625
+ });
626
+ }
627
+
628
+ document.getElementById('search-input').addEventListener('input', filterProducts);
629
+
630
  function openModal(index) {
631
  loadProductDetails(index);
632
  document.getElementById('productModal').style.display = "block";
 
791
  favoriteButton.classList.add('favorited');
792
  }
793
  localStorage.setItem('favorites', JSON.stringify(favorites));
794
+ loadFavorites();
795
  }
796
 
797
  function loadFavorites() {
 
804
  });
805
  }
806
 
 
 
 
 
807
  window.onclick = function(event) {
808
  if (event.target.className === 'modal') event.target.style.display = "none";
809
  }
810
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
811
  updateCartButton();
812
  loadFavorites();
813
+ filterProducts();
814
  </script>
815
  </body>
816
  </html>
817
  '''
818
+ return render_template_string(index_html, products=products, repo_id=REPO_ID)
819
 
820
  @app.route('/categories')
821
  def categories_page():
 
828
  <head>
829
  <meta charset="UTF-8">
830
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
831
+ <title>Каталог</title>
832
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
833
  <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
834
  <style>
 
883
  body.dark-mode .theme-toggle {
884
  color: #bbb;
885
  }
886
+ .category-list {
887
  display: grid;
888
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
889
  gap: 20px;
890
+ padding: 20px 0;
891
  }
892
  .category-item {
893
+ background: rgba(255, 255, 255, 0.9);
894
  border: 1px solid rgba(0, 0, 0, 0.05);
895
  border-radius: 15px;
896
+ padding: 15px;
897
  text-align: center;
898
  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
899
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
900
+ cursor: pointer;
901
  text-decoration: none;
902
  color: #333;
903
  }
 
 
 
 
 
904
  .category-item:hover {
905
  transform: translateY(-5px);
906
  box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
 
 
 
907
  }
908
+ body.dark-mode .category-item {
909
  background: rgba(42, 42, 62, 0.9);
910
+ border-color: rgba(255, 255, 255, 0.05);
911
+ color: #e0e0e0;
912
  }
913
  .category-item h2 {
914
  font-size: 1.2rem;
915
  font-weight: 500;
916
  }
917
+ #cart-button {
918
+ position: fixed;
919
+ bottom: 80px;
920
+ right: 20px;
921
+ background-color: #e63946;
922
+ color: white;
923
+ border: none;
924
+ border-radius: 50%;
925
+ width: 60px;
926
+ height: 60px;
927
+ font-size: 1.5rem;
928
+ cursor: pointer;
929
+ display: none;
930
+ box-shadow: 0 4px 15px rgba(230, 57, 70, 0.4);
931
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
932
+ z-index: 1000;
933
+ }
934
+ #cart-button:hover {
935
+ transform: scale(1.1);
936
+ }
937
+ .modal {
938
+ display: none;
939
+ position: fixed;
940
+ z-index: 1001;
941
+ left: 0;
942
+ top: 0;
943
+ width: 100%;
944
+ height: 100%;
945
+ background-color: rgba(0,0,0,0.5);
946
+ backdrop-filter: blur(5px);
947
+ }
948
+ .modal-content {
949
+ background: #fff;
950
+ margin: 5% auto;
951
+ padding: 20px;
952
+ border-radius: 15px;
953
+ width: 90%;
954
+ max-width: 700px;
955
+ box-shadow: 0 10px 30px rgba(0,0,0,0.2);
956
+ animation: slideIn 0.3s ease-out;
957
+ }
958
+ body.dark-mode .modal-content {
959
+ background: #2a2a3e;
960
+ color: #e0e0e0;
961
+ }
962
+ @keyframes slideIn {
963
+ from { transform: translateY(-50px); opacity: 0; }
964
+ to { transform: translateY(0); opacity: 1; }
965
+ }
966
+ .close {
967
+ float: right;
968
+ font-size: 1.5rem;
969
+ color: #666;
970
+ cursor: pointer;
971
+ transition: color 0.3s;
972
+ }
973
+ .close:hover {
974
+ color: #333;
975
+ }
976
+ body.dark-mode .close {
977
+ color: #bbb;
978
+ }
979
+ body.dark-mode .close:hover {
980
+ color: #fff;
981
+ }
982
+ .cart-item {
983
+ display: flex;
984
+ justify-content: space-between;
985
+ align-items: center;
986
+ padding: 15px 0;
987
+ border-bottom: 1px solid rgba(0, 0, 0, 0.1);
988
+ }
989
+ body.dark-mode .cart-item {
990
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
991
+ }
992
+ .cart-item img {
993
+ width: 50px;
994
+ height: 50px;
995
+ object-fit: contain;
996
+ border-radius: 8px;
997
+ margin-right: 15px;
998
+ }
999
+ .quantity-input, .color-select {
1000
+ width: 100%;
1001
+ max-width: 150px;
1002
+ padding: 8px;
1003
+ border: 1px solid rgba(0, 0, 0, 0.1);
1004
+ border-radius: 8px;
1005
+ font-size: 1rem;
1006
+ margin: 5px 0;
1007
+ }
1008
+ .clear-cart {
1009
+ background-color: #e63946;
1010
+ }
1011
+ .clear-cart:hover {
1012
+ background-color: #d62828;
1013
+ box-shadow: 0 4px 15px rgba(230, 57, 70, 0.4);
1014
+ }
1015
+ .order-button {
1016
+ background-color: #2ecc71;
1017
+ }
1018
+ .order-button:hover {
1019
+ background-color: #27ae60;
1020
+ box-shadow: 0 4px 15px rgba(46, 204, 113, 0.4);
1021
+ }
1022
  .navbar {
1023
  position: fixed;
1024
  bottom: 0;
 
1060
  <body>
1061
  <div class="container">
1062
  <div class="header">
1063
+ <h1>Каталог</h1>
1064
  <button class="theme-toggle" onclick="toggleTheme()">
1065
  <i class="fas fa-moon"></i>
1066
  </button>
1067
  </div>
1068
+ <div class="category-list">
1069
  {% for category in categories %}
1070
+ <a href="/category/{{ category|urlencode }}" class="category-item">
1071
  <h2>{{ category }}</h2>
1072
  </a>
1073
  {% endfor %}
1074
  </div>
1075
  </div>
1076
 
1077
+ <!-- Cart Modal -->
1078
+ <div id="cartModal" class="modal">
1079
  <div class="modal-content">
1080
+ <span class="close" onclick="closeModal('cartModal')">×</span>
1081
+ <h2>Корзина</h2>
1082
+ <div id="cartContent"></div>
1083
+ <div style="margin-top: 20px; text-align: right;">
1084
+ <strong>Итого: <span id="cartTotal">0</span> с</strong>
1085
+ <button class="product-button clear-cart" onclick="clearCart()">Очистить</button>
1086
+ <button class="product-button order-button" onclick="orderViaWhatsApp()">Заказать</button>
1087
+ </div>
1088
  </div>
1089
  </div>
1090
 
1091
+ <button id="cart-button" onclick="openCartModal()">🛒</button>
1092
+
1093
  <div class="navbar">
1094
  <a href="/">
1095
  <i class="fas fa-home"></i>
 
1107
  <i class="fas fa-tag"></i>
1108
  Скидки
1109
  </a>
 
 
 
 
1110
  <a href="https://api.whatsapp.com/send?phone=996500654659">
1111
  <i class="fab fa-whatsapp"></i>
1112
+ WhatsApp
1113
+ </a>
1114
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1115
 
1116
+ <script>
1117
+ function toggleTheme() {
1118
+ document.body.classList.toggle('dark-mode');
1119
+ const icon = document.querySelector('.theme-toggle i');
1120
+ icon.classList.toggle('fa-moon');
1121
+ icon.classList.toggle('fa-sun');
1122
+ localStorage.setItem('theme', document.body.classList.contains('dark-mode') ? 'dark' : 'light');
1123
+ }
1124
+
1125
+ if (localStorage.getItem('theme') === 'dark') {
1126
+ document.body.classList.add('dark-mode');
1127
+ document.querySelector('.theme-toggle i').classList.replace('fa-moon', 'fa-sun');
1128
  }
1129
 
1130
  function updateCartButton() {
 
1159
  document.getElementById('cartModal').style.display = 'block';
1160
  }
1161
 
1162
+ function closeModal(modalId) {
1163
+ document.getElementById(modalId).style.display = "none";
1164
+ }
1165
+
1166
  function orderViaWhatsApp() {
1167
  const cart = JSON.parse(localStorage.getItem('cart') || '[]');
1168
  if (cart.length === 0) {
 
1186
  updateCartButton();
1187
  }
1188
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1189
  window.onclick = function(event) {
1190
  if (event.target.className === 'modal') event.target.style.display = "none";
1191
  }
1192
 
1193
  updateCartButton();
 
1194
  </script>
1195
  </body>
1196
  </html>
1197
  '''
1198
+ return render_template_string(categories_html, categories=categories, repo_id=REPO_ID)
1199
 
1200
+ @app.route('/category/<category>')
1201
+ def category_page(category):
1202
  data = load_data()
1203
+ products = [p for p in data['products'] if p.get('category') == category]
 
1204
 
1205
+ category_html = '''
1206
  <!DOCTYPE html>
1207
  <html lang="ru">
1208
  <head>
1209
  <meta charset="UTF-8">
1210
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
1211
+ <title>{{ category }}</title>
1212
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
1213
  <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
1214
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.css">
 
1579
  </button>
1580
  </div>
1581
  <div class="products-grid" id="products-grid">
1582
+ <!-- Продукты будут добавлены через JavaScript -->
1583
  </div>
1584
  </div>
1585
 
 
1616
  </div>
1617
  </div>
1618
 
 
 
 
 
 
 
 
 
 
1619
  <button id="cart-button" onclick="openCartModal()">🛒</button>
1620
 
1621
  <div class="navbar">
 
1635
  <i class="fas fa-tag"></i>
1636
  Скидки
1637
  </a>
 
 
 
 
1638
  <a href="https://api.whatsapp.com/send?phone=996500654659">
1639
  <i class="fab fa-whatsapp"></i>
1640
  WhatsApp
 
1661
  document.querySelector('.theme-toggle i').classList.replace('fa-moon', 'fa-sun');
1662
  }
1663
 
1664
+ function loadFavorites() {
1665
  const favorites = JSON.parse(localStorage.getItem('favorites') || '[]');
1666
  const productsGrid = document.getElementById('products-grid');
1667
  productsGrid.innerHTML = '';
 
1674
  favorites.forEach(index => {
1675
  const product = products[index];
1676
  if (!product) return;
1677
+
1678
  const productElement = document.createElement('div');
1679
  productElement.className = 'product';
1680
  productElement.setAttribute('onclick', `openModal(${index})`);
1681
  productElement.setAttribute('data-name', product.name.toLowerCase());
1682
  productElement.setAttribute('data-description', product.description.toLowerCase());
1683
  productElement.setAttribute('data-category', product.category || 'Без категории');
1684
+
1685
+ let priceHtml = '';
1686
+ if (product.discount) {
1687
+ const discountedPrice = (product.price * (1 - product.discount / 100)).toFixed(2);
1688
+ priceHtml = `
1689
+ <span style="text-decoration: line-through; color: #666;">${product.price} с</span>
1690
+ <span>${discountedPrice} с</span>
1691
+ <span class="discount">Скидка: ${product.discount}%</span>
1692
+ `;
1693
+ } else {
1694
+ priceHtml = `<span>${product.price} с</span>`;
1695
+ }
1696
+ if (product.wholesale_price && product.min_wholesale) {
1697
+ priceHtml += `<span class="wholesale">Опт: ${product.wholesale_price} с</span>`;
1698
+ }
1699
+
1700
  productElement.innerHTML = `
1701
  <button class="favorite-button favorited" onclick="event.stopPropagation(); toggleFavorite(${index})">
1702
  <i class="fas fa-heart"></i>
1703
  </button>
1704
  ${product.photos && product.photos.length > 0 ? `
1705
  <div class="product-image">
1706
+ <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/${product.photos[0]}"
1707
+ alt="${product.name}"
1708
+ loading="lazy">
1709
  </div>` : ''}
1710
  ${product.wholesale_price && product.min_wholesale ? `<span class="wholesale-badge">Опт от ${product.min_wholesale}</span>` : ''}
1711
  ${product.discount ? `<span class="discount-badge">Скидка ${product.discount}%</span>` : ''}
1712
  <h2>${product.name}</h2>
1713
+ <div class="product-price">${priceHtml}</div>
 
 
 
 
 
 
1714
  <p class="product-description">${product.description.slice(0, 50)}${product.description.length > 50 ? '...' : ''}</p>
1715
  <button class="product-button add-to-cart" onclick="event.stopPropagation(); openQuantityModal(${index})">В корзину</button>
1716
  `;
 
1873
  function toggleFavorite(index) {
1874
  let favorites = JSON.parse(localStorage.getItem('favorites') || '[]');
1875
  const productId = index.toString();
 
1876
  if (favorites.includes(productId)) {
1877
  favorites = favorites.filter(id => id !== productId);
 
1878
  } else {
1879
  favorites.push(productId);
 
1880
  }
1881
  localStorage.setItem('favorites', JSON.stringify(favorites));
1882
+ loadFavorites();
 
 
 
 
1883
  }
1884
 
1885
  window.onclick = function(event) {
 
1887
  }
1888
 
1889
  updateCartButton();
1890
+ loadFavorites();
1891
  </script>
1892
  </body>
1893
  </html>
 
1898
  def discounts_page():
1899
  data = load_data()
1900
  products = [p for p in data['products'] if p.get('discount')]
 
1901
 
1902
  discounts_html = '''
1903
  <!DOCTYPE html>
 
2351
  </div>
2352
  </div>
2353
 
 
 
 
 
 
 
 
 
 
2354
  <button id="cart-button" onclick="openCartModal()">🛒</button>
2355
 
2356
  <div class="navbar">
 
2370
  <i class="fas fa-tag"></i>
2371
  Скидки
2372
  </a>
 
 
 
 
2373
  <a href="https://api.whatsapp.com/send?phone=996500654659">
2374
  <i class="fab fa-whatsapp"></i>
2375
  WhatsApp
 
2560
  favoriteButton.classList.add('favorited');
2561
  }
2562
  localStorage.setItem('favorites', JSON.stringify(favorites));
2563
+ loadFavorites();
2564
  }
2565
 
2566
  function loadFavorites() {
 
2573
  });
2574
  }
2575
 
 
 
 
 
2576
  window.onclick = function(event) {
2577
  if (event.target.className === 'modal') event.target.style.display = "none";
2578
  }
 
2583
  </body>
2584
  </html>
2585
  '''
2586
+ return render_template_string(discounts_html, products=products, repo_id=REPO_ID)
2587
 
2588
  @app.route('/product/<int:index>')
2589
  def product_details(index):
 
2594
  <style>
2595
  .swiper-container {
2596
  width: 100%;
2597
+ max-width: 500px;
2598
  margin: 20px auto;
2599
  }
2600
  .swiper-slide {
 
2646
  .product-details p {
2647
  font-size: 0.95rem;
2648
  color: #666;
2649
+ margin-bottom: 15px;
2650
  }
2651
  body.dark-mode .product-details p {
2652
  color: #bbb;
2653
  }
2654
+ .colors {
2655
  margin: 10px 0;
2656
  }
2657
+ .colors span {
2658
  display: inline-block;
2659
  margin-right: 10px;
2660
  padding: 5px 10px;
2661
+ border: 1px solid rgba(0, 0, 0, 0.1);
2662
+ border-radius: 5px;
2663
  font-size: 0.9rem;
2664
  }
2665
+ body.dark-mode .colors span {
2666
+ border-color: rgba(255, 255, 255, 0.1);
 
2667
  }
2668
  </style>
2669
  <div class="product-details">
 
2672
  <div class="swiper-wrapper">
2673
  {% for photo in product.get('photos', []) %}
2674
  <div class="swiper-slide">
2675
+ <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo }}"
2676
+ alt="{{ product['name'] }}"
2677
+ loading="lazy">
2678
  </div>
2679
  {% endfor %}
2680
  </div>
 
2694
  <span class="wholesale">Опт: {{ product['wholesale_price'] }} с (от {{ product['min_wholesale'] }})</span>
2695
  {% endif %}
2696
  </div>
2697
+ <p>{{ product['description'] }}</p>
2698
+ {% if product.get('colors') and product['colors']|length > 0 %}
 
2699
  <div class="colors">
2700
  <strong>Цвета:</strong>
2701
  {% for color in product['colors'] %}
 
2707
  '''
2708
  return render_template_string(product_html, product=product, repo_id=REPO_ID)
2709
 
2710
+ # Запуск периодического бэкапа в отдельном потоке
2711
+ backup_thread = threading.Thread(target=periodic_backup, daemon=True)
2712
+ backup_thread.start()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2713
 
2714
  if __name__ == '__main__':
 
 
 
 
 
 
2715
  app.run(debug=True, host='0.0.0.0', port=7860)
2716