Kgshop commited on
Commit
0855e97
·
verified ·
1 Parent(s): b566e1e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +473 -811
app.py CHANGED
@@ -1,16 +1,12 @@
1
  import os
2
- import io
3
  import base64
4
  import json
5
- import logging
6
  import threading
7
  import time
8
  from datetime import datetime
9
  from uuid import uuid4
10
 
11
- from flask import Flask, render_template_string, request, redirect, url_for, flash, jsonify, Response
12
- from PIL import Image
13
- import numpy as np
14
  from huggingface_hub import HfApi, hf_hub_download
15
  from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError
16
  from werkzeug.utils import secure_filename
@@ -20,31 +16,27 @@ import requests
20
  load_dotenv()
21
 
22
  app = Flask(__name__)
23
- app.secret_key = 'your_unique_secret_key_gippo_312_shop_54321_no_login'
24
  DATA_FILE = 'data.json'
25
  SYNC_FILES = [DATA_FILE]
26
- REPO_ID = "Kgshop/aiexample"
 
27
  HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
28
  HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
29
- STORE_ADDRESS = "Рынок Кербент, 6 ряд , 43 контейнер "
30
- WHATSAPP_NUMBER = "+996701202013"
31
- CURRENCY_CODE = 'KGS'
32
- DOWNLOAD_RETRIES = 3
33
- DOWNLOAD_DELAY = 5
34
 
35
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
 
36
 
37
- def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY):
38
- if not HF_TOKEN_READ and not HF_TOKEN_WRITE:
39
- logging.warning("HF_TOKEN_READ/HF_TOKEN_WRITE not set. Download might fail for private repos.")
40
  token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE
41
  files_to_download = [specific_file] if specific_file else SYNC_FILES
42
  all_successful = True
 
43
  for file_name in files_to_download:
44
  success = False
45
  for attempt in range(retries + 1):
46
  try:
47
- local_path = hf_hub_download(
48
  repo_id=REPO_ID,
49
  filename=file_name,
50
  repo_type="dataset",
@@ -65,20 +57,21 @@ def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWN
65
  if file_name == DATA_FILE:
66
  with open(file_name, 'w', encoding='utf-8') as f:
67
  json.dump({'products': [], 'categories': [], 'orders': {}}, f)
68
- except Exception as create_e:
69
  pass
70
  success = False
71
  break
72
- else:
73
- pass
74
- except requests.exceptions.RequestException as e:
75
- pass
76
- except Exception as e:
77
- pass
78
  if attempt < retries:
79
  time.sleep(delay)
 
80
  if not success:
81
  all_successful = False
 
82
  return all_successful
83
 
84
  def upload_db_to_hf(specific_file=None):
@@ -87,6 +80,7 @@ def upload_db_to_hf(specific_file=None):
87
  try:
88
  api = HfApi()
89
  files_to_upload = [specific_file] if specific_file else SYNC_FILES
 
90
  for file_name in files_to_upload:
91
  if os.path.exists(file_name):
92
  try:
@@ -98,17 +92,14 @@ def upload_db_to_hf(specific_file=None):
98
  token=HF_TOKEN_WRITE,
99
  commit_message=f"Sync {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
100
  )
101
- except Exception as e:
102
- pass
103
- else:
104
- pass
105
- except Exception as e:
106
  pass
107
 
108
  def periodic_backup():
109
- backup_interval = 1800
110
  while True:
111
- time.sleep(backup_interval)
112
  upload_db_to_hf()
113
 
114
  def load_data():
@@ -118,36 +109,34 @@ def load_data():
118
  with open(DATA_FILE, 'r', encoding='utf-8') as file:
119
  data = json.load(file)
120
  if not isinstance(data, dict):
121
- raise FileNotFoundError
122
  if 'products' not in data: data['products'] = []
123
  if 'categories' not in data: data['categories'] = []
124
  if 'orders' not in data: data['orders'] = {}
125
- except FileNotFoundError:
126
  if download_db_from_hf(specific_file=DATA_FILE):
127
  try:
128
  with open(DATA_FILE, 'r', encoding='utf-8') as file:
129
  data = json.load(file)
130
- if not isinstance(data, dict):
131
- data = default_data
132
  if 'products' not in data: data['products'] = []
133
  if 'categories' not in data: data['categories'] = []
134
  if 'orders' not in data: data['orders'] = {}
135
- except (FileNotFoundError, json.JSONDecodeError, Exception) as e:
136
  data = default_data
137
  else:
138
  data = default_data
139
- except (json.JSONDecodeError, Exception):
140
  data = default_data
 
141
  for product in data['products']:
142
  if 'product_id' not in product:
143
  product['product_id'] = uuid4().hex
144
- if any('product_id' not in p for p in data['products']):
145
- save_data(data)
146
  if not os.path.exists(DATA_FILE):
147
  try:
148
  with open(DATA_FILE, 'w', encoding='utf-8') as f:
149
  json.dump(default_data, f)
150
- except Exception as create_e:
151
  pass
152
  return data
153
 
@@ -158,778 +147,481 @@ def save_data(data):
158
  if 'products' not in data: data['products'] = []
159
  if 'categories' not in data: data['categories'] = []
160
  if 'orders' not in data: data['orders'] = {}
 
161
  with open(DATA_FILE, 'w', encoding='utf-8') as file:
162
  json.dump(data, file, ensure_ascii=False, indent=4)
163
  upload_db_to_hf(specific_file=DATA_FILE)
164
- except Exception as e:
165
  pass
166
 
167
- CATEGORY_PAGE_TEMPLATE = '''
 
168
  <!DOCTYPE html>
169
  <html lang="ru">
170
  <head>
171
  <meta charset="UTF-8">
172
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
173
- <title>Каталог</title>
174
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
175
  <style>
176
- body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 0; background-color: #f0f2f5; color: #1c1e21; padding-bottom: 80px; }
177
- .header { background-color: #fff; padding: 10px 16px; border-bottom: 1px solid #dddfe2; text-align: center; font-size: 1.5rem; font-weight: 600; position: sticky; top: 0; z-index: 100; }
178
- .container { padding: 8px; }
179
- .search-container { padding: 8px; }
180
- .search-bar { width: 100%; padding: 12px; font-size: 1rem; border-radius: 20px; border: 1px solid #ccd0d5; box-sizing: border-box; }
181
- .category-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
182
- .category-card { display: flex; flex-direction: column; justify-content: center; align-items: center; padding: 20px 16px; background-color: #fff; border: 1px solid #dddfe2; border-radius: 12px; text-decoration: none; color: #1c1e21; font-size: 1rem; font-weight: 500; transition: background-color 0.2s, transform 0.2s; text-align: center; }
183
- .category-card:active { background-color: #e7f3ff; transform: scale(0.98); }
184
- .product-count { background-color: #e4e6eb; color: #606770; font-size: 0.8rem; padding: 4px 10px; border-radius: 12px; font-weight: 600; margin-top: 8px; }
185
- .floating-cart-button { position: fixed; bottom: 20px; right: 20px; background-color: #007bff; color: white; width: 60px; height: 60px; border-radius: 50%; border: none; display: flex; align-items: center; justify-content: center; font-size: 1.5rem; box-shadow: 0 4px 12px rgba(0,0,0,0.15); cursor: pointer; z-index: 1000; }
186
- .cart-count { position: absolute; top: 0; right: 0; background-color: #dc3545; color: white; font-size: 0.75rem; width: 20px; height: 20px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; }
187
- .modal { display: none; position: fixed; z-index: 1001; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); }
188
- .modal-content { background-color: #fefefe; margin: auto; padding: 20px; border: 1px solid #888; width: 90%; max-width: 500px; border-radius: 8px; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); }
189
- .close { color: #aaa; float: right; font-size: 28px; font-weight: bold; cursor: pointer; }
190
- .cart-item { display: flex; justify-content: space-between; align-items: center; padding: 10px 0; border-bottom: 1px solid #eee; }
191
- .cart-item-info { font-size: 0.9rem; }
192
- .cart-total { text-align: right; margin-top: 15px; font-weight: bold; font-size: 1.1rem; }
193
- .formulate-order-button { width: 100%; padding: 12px; background-color: #28a745; color: white; border: none; border-radius: 6px; font-size: 1rem; font-weight: bold; cursor: pointer; margin-top: 15px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
  </style>
195
  </head>
196
  <body>
197
  <div class="header">
198
- Категории
199
- </div>
200
- <div class="container">
201
- <div class="search-container">
202
- <form action="{{ url_for('search') }}" method="get">
203
- <input type="search" name="q" class="search-bar" placeholder="&#xF002; Поиск по всем товарам" style="font-family: Arial, FontAwesome;">
204
- </form>
205
- </div>
206
- <div class="category-grid">
207
- {% for category, count in categories_with_counts.items() %}
208
- <a href="{{ url_for('product_list', category_name=category) }}" class="category-card">
209
- <span>{{ category }}</span>
210
- <span class="product-count">{{ count }}</span>
211
- </a>
212
- {% endfor %}
213
  </div>
 
214
  </div>
215
- <button id="cart-button" class="floating-cart-button">
216
- <i class="fas fa-shopping-cart"></i>
217
- <span class="cart-count" id="cart-count">0</span>
218
- </button>
219
- <div id="cartModal" class="modal">
220
- <div class="modal-content">
221
- <span class="close" id="closeCartModal">&times;</span>
222
- <h2>Корзина</h2>
223
- <div id="cart-content"></div>
224
- <p class="cart-total">Итого: <span id="cart-total">0</span> {{ currency_code }}</p>
225
- <button class="formulate-order-button" id="formulate-order-btn">Сформировать заказ</button>
226
- </div>
227
  </div>
228
- <script>
229
- const currencyCode = '{{ currency_code }}';
230
- let cart = JSON.parse(localStorage.getItem('gippoCart') || '[]');
231
-
232
- function updateUI() {
233
- let totalItems = 0;
234
- cart.forEach(item => {
235
- totalItems += item.quantity;
236
- });
237
- const cartCountEl = document.getElementById('cart-count');
238
- cartCountEl.textContent = totalItems;
239
- cartCountEl.style.display = totalItems > 0 ? 'flex' : 'none';
240
- }
241
 
242
- const modal = document.getElementById('cartModal');
243
- const cartButton = document.getElementById('cart-button');
244
- const closeButton = document.getElementById('closeCartModal');
245
 
246
- cartButton.onclick = function() {
247
- renderCartModal();
248
- modal.style.display = "block";
249
- }
250
- closeButton.onclick = function() {
251
- modal.style.display = "none";
252
- }
253
- window.onclick = function(event) {
254
- if (event.target == modal) {
255
- modal.style.display = "none";
256
- }
257
- }
258
- function renderCartModal() {
259
- const cartContent = document.getElementById('cart-content');
260
- const cartTotalEl = document.getElementById('cart-total');
261
- let total = 0;
262
- if (cart.length === 0) {
263
- cartContent.innerHTML = '<p>Корзина пуста</p>';
264
- cartTotalEl.textContent = '0';
265
- } else {
266
- cartContent.innerHTML = cart.map(item => {
267
- const itemTotal = item.price * item.quantity;
268
- total += itemTotal;
269
- return `<div class="cart-item"><span class="cart-item-info">${item.name} x ${item.quantity}</span> <span>${itemTotal.toFixed(0)}</span></div>`;
270
- }).join('');
271
- cartTotalEl.textContent = total.toFixed(0);
272
- }
273
- }
274
- document.getElementById('formulate-order-btn').addEventListener('click', () => {
275
- if (cart.length === 0) {
276
- alert("Корзина пуста!");
277
- return;
278
- }
279
- fetch('/create_order', {
280
- method: 'POST',
281
- headers: { 'Content-Type': 'application/json' },
282
- body: JSON.stringify({ cart: cart })
283
- })
284
- .then(response => response.json())
285
- .then(data => {
286
- if (data.order_id) {
287
- localStorage.removeItem('gippoCart');
288
- window.location.href = `/order/${data.order_id}`;
289
- } else {
290
- alert('Ошибка при создании заказа.');
291
- }
292
- })
293
- .catch(error => alert('Ошибка сети.'));
294
- });
295
- document.addEventListener('DOMContentLoaded', updateUI);
296
- </script>
297
- </body>
298
- </html>
299
- '''
300
-
301
- PRODUCT_PAGE_TEMPLATE = '''
302
- <!DOCTYPE html>
303
- <html lang="ru">
304
- <head>
305
- <meta charset="UTF-8">
306
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
307
- <title>{{ title }}</title>
308
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
309
- <style>
310
- body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 0; background-color: #f0f2f5; color: #1c1e21; padding-bottom: 80px; }
311
- .header { display: flex; align-items: center; background-color: #fff; padding: 10px 16px; border-bottom: 1px solid #dddfe2; position: sticky; top: 0; z-index: 100; }
312
- .header a { color: #007bff; text-decoration: none; font-size: 1.2rem; }
313
- .header h1 { font-size: 1.2rem; font-weight: 600; margin: 0; position: absolute; left: 50%; transform: translateX(-50%); }
314
- .search-container { padding: 8px 16px; background-color: #fff; border-bottom: 1px solid #dddfe2; }
315
- .search-bar { width: 100%; padding: 10px; font-size: 1rem; border-radius: 20px; border: 1px solid #ccd0d5; box-sizing: border-box; }
316
- .product-list { display: flex; flex-direction: column; gap: 8px; padding: 8px; }
317
- .product-card { display: flex; align-items: center; padding: 12px; background-color: #fff; border: 1px solid #dddfe2; border-radius: 12px; gap: 12px; }
318
- .product-card img { width: 80px; height: 80px; object-fit: cover; border-radius: 8px; flex-shrink: 0; cursor: pointer; }
319
- .product-info { flex-grow: 1; }
320
- .product-name { font-size: 1rem; font-weight: 500; margin: 0 0 4px 0; }
321
- .product-price { font-size: 0.9rem; color: #606770; margin: 0; }
322
- .quantity-selector { display: flex; align-items: center; gap: 10px; }
323
- .quantity-btn { width: 32px; height: 32px; border: 1px solid #ccc; background-color: #f8f9fa; border-radius: 50%; font-size: 1.5rem; line-height: 1; color: #007bff; cursor: pointer; transition: background-color 0.2s; }
324
- .quantity-btn:active { background-color: #e2e6ea; }
325
- .quantity-input { width: 40px; text-align: center; border: 1px solid #ccc; border-radius: 4px; padding: 5px; font-size: 1rem; -moz-appearance: textfield; }
326
- .quantity-input::-webkit-outer-spin-button, .quantity-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
327
- .floating-cart-button { position: fixed; bottom: 20px; right: 20px; background-color: #007bff; color: white; width: 60px; height: 60px; border-radius: 50%; border: none; display: flex; align-items: center; justify-content: center; font-size: 1.5rem; box-shadow: 0 4px 12px rgba(0,0,0,0.15); cursor: pointer; z-index: 1000; }
328
- .cart-count { position: absolute; top: 0; right: 0; background-color: #dc3545; color: white; font-size: 0.75rem; width: 20px; height: 20px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; }
329
- .modal { display: none; position: fixed; z-index: 1001; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); }
330
- .modal-content { background-color: #fefefe; margin: auto; padding: 20px; border: 1px solid #888; width: 90%; max-width: 500px; border-radius: 8px; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); }
331
- .close { color: #aaa; float: right; font-size: 28px; font-weight: bold; cursor: pointer; }
332
- .cart-item { display: flex; justify-content: space-between; align-items: center; padding: 10px 0; border-bottom: 1px solid #eee; }
333
- .cart-item-info { font-size: 0.9rem; }
334
- .cart-total { text-align: right; margin-top: 15px; font-weight: bold; font-size: 1.1rem; }
335
- .formulate-order-button { width: 100%; padding: 12px; background-color: #28a745; color: white; border: none; border-radius: 6px; font-size: 1rem; font-weight: bold; cursor: pointer; margin-top: 15px; }
336
- #gallery-modal { background-color: rgba(0,0,0,0.9); z-index: 2000; }
337
- #gallery-modal .close { color: #fff; font-size: 40px; position: absolute; top: 15px; right: 35px; }
338
- .gallery-content { position: relative; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; }
339
- .gallery-content img { max-width: 90%; max-height: 80%; object-fit: contain; }
340
- .gallery-nav { cursor: pointer; position: absolute; top: 50%; transform: translateY(-50%); color: white; font-size: 3rem; padding: 16px; user-select: none; }
341
- #prev-btn { left: 0; }
342
- #next-btn { right: 0; }
343
- </style>
344
- </head>
345
- <body>
346
- <div class="header">
347
- <a href="{{ url_for('catalog') }}"><i class="fas fa-chevron-left"></i></a>
348
- <h1>{{ title }}</h1>
349
- </div>
350
- <div class="search-container">
351
- <input type="search" id="product-search" class="search-bar" placeholder="&#xF002; Поиск в этой категории..." style="font-family: Arial, FontAwesome;">
352
  </div>
353
- <div class="product-list" id="product-list">
354
- {% if not products %}
355
- <p style="text-align:center; padding: 20px;">Товары не найдены.</p>
356
- {% endif %}
357
- {% for product in products %}
358
- <div class="product-card" data-product-id="{{ product.product_id }}" data-product-name="{{ product.name }}" data-product-desc="{{ product.description }}">
359
- {% if product.get('photos') and product['photos']|length > 0 %}
360
- <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product['photos'][0] }}" alt="{{ product.name }}" class="product-image" data-photos='{{ product.photos|tojson }}'>
361
- {% else %}
362
- <img src="https://via.placeholder.com/80x80.png?text=N/A" alt="No Image">
363
- {% endif %}
364
- <div class="product-info">
365
- <p class="product-name">{{ product.name }}</p>
366
- <p class="product-price">{{ "%.0f"|format(product.price) }} {{ currency_code }}</p>
367
- </div>
368
- <div class="quantity-selector">
369
- <button class="quantity-btn minus">-</button>
370
- <input type="number" value="0" min="0" class="quantity-input" readonly>
371
- <button class="quantity-btn plus">+</button>
372
- </div>
373
  </div>
374
- {% endfor %}
375
  </div>
376
 
377
- <button id="cart-button" class="floating-cart-button">
378
- <i class="fas fa-shopping-cart"></i>
379
- <span class="cart-count" id="cart-count">0</span>
380
- </button>
381
- <div id="cartModal" class="modal">
382
- <div class="modal-content">
383
- <span class="close" id="closeCartModal">&times;</span>
384
- <h2>Корзина</h2>
385
- <div id="cart-content"></div>
386
- <p class="cart-total">Итого: <span id="cart-total">0</span> {{ currency_code }}</p>
387
- <button class="formulate-order-button" id="formulate-order-btn">Сформировать заказ</button>
388
- </div>
389
- </div>
390
- <div id="gallery-modal" class="modal">
391
- <span class="close" id="closeGalleryModal">&times;</span>
392
- <div class="gallery-content">
393
- <span class="gallery-nav" id="prev-btn">&#10094;</span>
394
- <img id="gallery-image" src="">
395
- <span class="gallery-nav" id="next-btn">&#10095;</span>
396
  </div>
397
  </div>
398
 
399
  <script>
400
- const allProducts = {{ products_json|safe }};
 
401
  const repoId = '{{ repo_id }}';
402
- const currencyCode = '{{ currency_code }}';
403
- let cart = JSON.parse(localStorage.getItem('gippoCart') || '[]');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
404
 
405
- function getProductById(productId) {
406
- return allProducts.find(p => p.product_id === productId);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
407
  }
408
 
409
  function updateCart(productId, change) {
410
- const product = getProductById(productId);
411
  if (!product) return;
412
- const existingItemIndex = cart.findIndex(item => item.product_id === productId);
413
- let newQuantity;
414
- if (existingItemIndex > -1) {
415
- newQuantity = cart[existingItemIndex].quantity + change;
416
- } else {
417
- newQuantity = change > 0 ? change : 0;
418
  }
419
- if (newQuantity > 0) {
420
- if (existingItemIndex > -1) {
421
- cart[existingItemIndex].quantity = newQuantity;
422
- } else {
423
- cart.push({ product_id: product.product_id, name: product.name, price: product.price, photo: product.photos && product.photos.length > 0 ? product.photos[0] : null, quantity: newQuantity });
424
- }
425
- } else if (existingItemIndex > -1) {
426
- cart.splice(existingItemIndex, 1);
427
  }
428
- localStorage.setItem('gippoCart', JSON.stringify(cart));
429
- updateUI();
430
- }
431
-
432
- function updateUI() {
433
- let totalItems = 0;
434
- cart.forEach(item => {
435
- const productCard = document.querySelector(`.product-card[data-product-id="${item.product_id}"]`);
436
- if (productCard) {
437
- productCard.querySelector('.quantity-input').value = item.quantity;
438
- }
439
- totalItems += item.quantity;
440
- });
441
- document.querySelectorAll('.product-card').forEach(card => {
442
- const productId = card.dataset.productId;
443
- if (!cart.some(item => item.product_id === productId)) {
444
- card.querySelector('.quantity-input').value = 0;
445
- }
446
- });
447
- const cartCountEl = document.getElementById('cart-count');
448
- cartCountEl.textContent = totalItems;
449
- cartCountEl.style.display = totalItems > 0 ? 'flex' : 'none';
450
  }
451
 
452
- document.getElementById('product-list').addEventListener('click', (e) => {
453
- const card = e.target.closest('.product-card');
454
- if (!card) return;
455
- const productId = card.dataset.productId;
456
- if (e.target.classList.contains('plus')) {
457
- updateCart(productId, 1);
458
- } else if (e.target.classList.contains('minus')) {
459
- updateCart(productId, -1);
460
- }
461
- });
462
-
463
- const cartModal = document.getElementById('cartModal');
464
- const cartButton = document.getElementById('cart-button');
465
- const closeCartButton = document.getElementById('closeCartModal');
466
- cartButton.onclick = function() { renderCartModal(); cartModal.style.display = "block"; }
467
- closeCartButton.onclick = function() { cartModal.style.display = "none"; }
468
- window.addEventListener('click', function(event) {
469
- if (event.target == cartModal) { cartModal.style.display = "none"; }
470
- });
471
-
472
- function renderCartModal() {
473
- const cartContent = document.getElementById('cart-content');
474
- const cartTotalEl = document.getElementById('cart-total');
475
  let total = 0;
476
- if (cart.length === 0) {
477
- cartContent.innerHTML = '<p>Корзина пуста</p>';
478
- cartTotalEl.textContent = '0';
 
 
 
 
 
479
  } else {
480
- cartContent.innerHTML = cart.map(item => {
481
- const itemTotal = item.price * item.quantity;
482
- total += itemTotal;
483
- return `<div class="cart-item"><span class="cart-item-info">${item.name} x ${item.quantity}</span> <span>${itemTotal.toFixed(0)}</span></div>`;
484
- }).join('');
485
- cartTotalEl.textContent = total.toFixed(0);
486
  }
487
  }
488
 
489
- document.getElementById('formulate-order-btn').addEventListener('click', () => {
490
- if (cart.length === 0) { alert("Корзина пуста!"); return; }
491
- fetch('/create_order', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ cart: cart }) })
492
- .then(response => response.json())
493
- .then(data => {
494
- if (data.order_id) { localStorage.removeItem('gippoCart'); window.location.href = `/order/${data.order_id}`; }
495
- else { alert('Ошибка при создании заказа.'); }
496
- }).catch(error => alert('Ошибка сети.'));
497
- });
498
-
499
- document.getElementById('product-search').addEventListener('input', function(e) {
500
- const searchTerm = e.target.value.toLowerCase();
501
- document.querySelectorAll('.product-card').forEach(card => {
502
- const name = card.dataset.productName.toLowerCase();
503
- const desc = card.dataset.productDesc.toLowerCase();
504
- if (name.includes(searchTerm) || desc.includes(searchTerm)) {
505
- card.style.display = 'flex';
506
- } else {
507
- card.style.display = 'none';
508
- }
509
- });
510
- });
511
-
512
- const galleryModal = document.getElementById('gallery-modal');
513
- const galleryImage = document.getElementById('gallery-image');
514
- const closeGalleryButton = document.getElementById('closeGalleryModal');
515
- const prevBtn = document.getElementById('prev-btn');
516
- const nextBtn = document.getElementById('next-btn');
517
- let currentPhotos = [];
518
- let currentIndex = 0;
519
-
520
- function updateGalleryImage() {
521
- if (currentPhotos.length > 0) {
522
- galleryImage.src = `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${currentPhotos[currentIndex]}`;
523
- prevBtn.style.display = currentPhotos.length > 1 ? 'block' : 'none';
524
- nextBtn.style.display = currentPhotos.length > 1 ? 'block' : 'none';
525
  }
 
526
  }
527
- function showNext() { currentIndex = (currentIndex + 1) % currentPhotos.length; updateGalleryImage(); }
528
- function showPrev() { currentIndex = (currentIndex - 1 + currentPhotos.length) % currentPhotos.length; updateGalleryImage(); }
529
-
530
- document.getElementById('product-list').addEventListener('click', e => {
531
- if (e.target.classList.contains('product-image')) {
532
- const photos = JSON.parse(e.target.dataset.photos);
533
- if (photos && photos.length > 0) {
534
- currentPhotos = photos;
535
- currentIndex = 0;
536
- updateGalleryImage();
537
- galleryModal.style.display = 'block';
 
 
 
 
 
 
538
  }
539
- }
540
- });
541
-
542
- closeGalleryButton.onclick = () => { galleryModal.style.display = 'none'; };
543
- nextBtn.onclick = showNext;
544
- prevBtn.onclick = showPrev;
545
- let touchstartX = 0;
546
- let touchendX = 0;
547
- galleryModal.addEventListener('touchstart', e => { touchstartX = e.changedTouches[0].screenX; }, false);
548
- galleryModal.addEventListener('touchend', e => { touchendX = e.changedTouches[0].screenX; handleSwipe(); }, false);
549
- function handleSwipe() {
550
- if (touchendX < touchstartX) showNext();
551
- if (touchendX > touchstartX) showPrev();
552
  }
553
 
554
- document.addEventListener('DOMContentLoaded', updateUI);
555
  </script>
556
  </body>
557
  </html>
558
  '''
559
 
560
- ORDER_PAGE_TEMPLATE = '''
561
  <!DOCTYPE html>
562
  <html lang="ru">
563
  <head>
564
  <meta charset="UTF-8">
565
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
566
  <title>Накладная №{{ order.id }}</title>
567
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
568
  <style>
569
- body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; margin: 0; background-color: #f0f2f5; padding-bottom: 100px; }
570
- .invoice-box { max-width: 800px; margin: 20px auto; padding: 20px; background: #fff; border: 1px solid #eee; box-shadow: 0 0 10px rgba(0, 0, 0, 0.15); font-size: 16px; line-height: 24px; color: #555; }
571
- .invoice-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 20px; }
572
- .invoice-header h1 { font-size: 2em; margin: 0; font-weight: 600; color: #333; }
573
- .invoice-details p { margin: 0; font-size: 0.9em; }
574
- .invoice-table { width: 100%; border-collapse: collapse; }
575
- .invoice-table th, .invoice-table td { border: 1px solid #ddd; padding: 8px; text-align: left; }
576
- .invoice-table th { background-color: #f2f2f2; font-weight: 600; text-align: center; }
577
- .invoice-table td { vertical-align: middle; }
578
- .invoice-table img { max-width: 50px; max-height: 50px; object-fit: cover; border-radius: 4px; }
579
- .invoice-table .center { text-align: center; }
580
- .invoice-table .right { text-align: right; }
581
- .total-row td { border-top: 2px solid #333; font-weight: bold; }
582
- .floating-buttons { position: fixed; bottom: 0; left: 0; width: 100%; background-color: #fff; box-shadow: 0 -2px 10px rgba(0,0,0,0.1); display: flex; justify-content: space-around; padding: 15px 10px; box-sizing: border-box; z-index: 100; }
583
- .action-button { padding: 12px 20px; border: none; border-radius: 25px; color: white; font-weight: bold; cursor: pointer; transition: background-color 0.3s; font-size: 1rem; display: inline-flex; align-items: center; gap: 8px; text-decoration: none; box-shadow: 0 4px 10px rgba(0,0,0,0.2); flex-grow: 1; justify-content: center; margin: 0 5px; }
584
- .whatsapp-btn { background-color: #25D366; }
585
- .print-btn { background-color: #007BFF; }
586
  @media print {
587
- body { background-color: #fff; margin: 0; padding-bottom: 0; }
588
- .invoice-box { box-shadow: none; border: none; margin: 0 auto; max-width: 100%; }
589
  .floating-buttons { display: none; }
590
  }
591
  </style>
592
  </head>
593
  <body>
594
- {% if order %}
595
  <div class="invoice-box">
596
- <div class="invoice-header">
 
 
 
597
  <h1>Накладная</h1>
598
- <div class="invoice-details">
599
- <p><strong>NO:</strong> {{ order.id }}</p>
600
- <p><strong>дата:</strong> {{ order.created_at.split(' ')[0] }}</p>
601
- <p><strong>покупатель:</strong> Розничный</p>
602
  </div>
603
  </div>
604
- <table class="invoice-table">
 
 
 
 
 
 
 
 
 
 
605
  <thead>
606
  <tr>
607
  <th>NO</th>
608
  <th>Наименование</th>
609
  <th>Фото</th>
610
- <th class="center">кол-во</th>
611
- <th class="right">Цена</th>
612
- <th class="right">Сумма</th>
613
  </tr>
614
  </thead>
615
  <tbody>
616
  {% for item in order.cart %}
617
  <tr>
618
- <td class="center">{{ loop.index }}</td>
619
- <td>{{ item.name }}{% if item.color and item.color != 'N/A' %} ({{ item.color }}){% endif %}</td>
620
- <td class="center"><img src="{{ item.photo_url }}" alt="{{ item.name }}"></td>
621
- <td class="center">{{ item.quantity }}</td>
622
- <td class="right">{{ "%.0f"|format(item.price) }}</td>
623
- <td class="right">{{ "%.0f"|format(item.price * item.quantity) }}</td>
624
  </tr>
625
  {% endfor %}
626
- </tbody>
627
- <tfoot>
628
  <tr class="total-row">
629
- <td colspan="5" class="right"><strong>Итого</strong></td>
630
- <td class="right"><strong>{{ "%.0f"|format(order.total_price) }}</strong></td>
631
  </tr>
632
- </tfoot>
633
  </table>
634
  </div>
 
635
  <div class="floating-buttons">
636
- <button class="action-button print-btn" onclick="window.print()"><i class="fas fa-print"></i> Печать</button>
637
- <button class="action-button whatsapp-btn" onclick="sendOrderViaWhatsApp()"><i class="fab fa-whatsapp"></i> WhatsApp</button>
638
  </div>
 
639
  <script>
640
- function sendOrderViaWhatsApp() {
641
- const orderId = "{{ order.id }}";
642
- const whatsappNumber = "{{ whatsapp_number }}";
643
- let message = `*Заказ № ${orderId}*%0A%0A`;
644
- let total = 0;
645
  {% for item in order.cart %}
646
- message += `{{ loop.index }}. {{ item.name }}%0A`;
647
- message += ` - Кол-во: {{ item.quantity }}%0A`;
648
- message += ` - Сумма: {{ "%.0f"|format(item.price * item.quantity) }} {{ currency_code }}%0A`;
649
- total += {{ item.price * item.quantity }};
650
  {% endfor %}
651
- message += `%0A*Итого:* ${total.toFixed(0)} {{ currency_code }}`;
652
- const whatsappUrl = `https://api.whatsapp.com/send?phone=${whatsappNumber}&text=${message}`;
653
- window.open(whatsappUrl, '_blank');
654
  }
655
  </script>
656
- {% else %}
657
- <div class="invoice-box" style="text-align:center;">
658
- <h1>Заказ не найден</h1>
659
- <p>К сожалению, заказ с таким ID не существует.</p>
660
- <a href="{{ url_for('catalog') }}">Вернуться в каталог</a>
661
- </div>
662
- {% endif %}
663
  </body>
664
  </html>
665
  '''
666
 
667
- ADMIN_PAGE_TEMPLATE = '''
668
  <!DOCTYPE html>
669
  <html lang="ru">
670
  <head>
671
  <meta charset="UTF-8">
672
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
673
  <title>Админ-панель</title>
674
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
675
  <style>
676
- body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background-color: #f4f6f9; color: #333; margin: 0; padding: 15px; }
677
- .container { max-width: 900px; margin: 0 auto; background-color: #fff; padding: 20px; border-radius: 10px; box-shadow: 0 3px 10px rgba(0,0,0,0.05); }
678
- .header { display: flex; justify-content: space-between; align-items: center; padding-bottom: 15px; margin-bottom: 20px; border-bottom: 1px solid #e0e0e0; }
679
- h1, h2 { color: #003C43; }
680
- h1 { font-size: 1.6rem; font-weight: 600; }
681
- h2 { font-size: 1.4rem; margin-top: 25px; margin-bottom: 15px; }
682
- .section { margin-bottom: 25px; }
683
- label { font-weight: 500; margin-top: 10px; display: block; color: #555; font-size: 0.9rem; }
684
- input[type="text"], input[type="number"], textarea, select, .search-bar { width: 100%; padding: 10px; margin-top: 5px; border: 1px solid #ccc; border-radius: 6px; box-sizing: border-box; }
685
- .search-bar { margin-bottom: 15px; }
686
- button, .button { padding: 10px 18px; border: none; border-radius: 6px; background-color: #007bff; color: white; font-weight: 600; cursor: pointer; transition: background-color 0.3s; margin-top: 15px; font-size: 0.95rem; text-decoration: none; }
687
- button:hover, .button:hover { background-color: #0056b3; }
688
- .delete-btn { background-color: #dc3545; }
689
- .delete-btn:hover { background-color: #c82333; }
690
- .edit-btn { background-color: #ffc107; color: #212529; }
691
- .edit-btn:hover { background-color: #e0a800; }
692
- .message { padding: 12px 15px; border-radius: 6px; margin-bottom: 20px; font-size: 0.95rem; }
693
- .message.success { background-color: #d4edda; color: #155724; }
694
- .message.error { background-color: #f8d7da; color: #721c24; }
695
- .category-block { margin-bottom: 20px; border: 1px solid #e0e0e0; border-radius: 8px; }
696
- .category-header { display: flex; justify-content: space-between; align-items: center; padding: 15px; background-color: #f8f9fa; border-bottom: 1px solid #e0e0e0; }
697
- .category-header h3 { margin: 0; font-size: 1.2rem; }
698
- .category-content { padding: 15px; }
699
- .product-item { display: flex; align-items: center; gap: 15px; padding: 10px 0; border-bottom: 1px solid #f0f0f0; }
700
- .product-item:last-child { border-bottom: none; }
701
- .product-item img { width: 50px; height: 50px; object-fit: cover; border-radius: 5px; }
702
- .product-info { flex-grow: 1; }
703
- .product-actions button, .product-actions .button { margin: 0 0 0 5px; padding: 6px 10px; font-size: 0.8rem; }
704
- .modal { display: none; position: fixed; z-index: 1050; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.5); }
705
- .modal-content { background-color: #fefefe; margin: 10% auto; padding: 20px; border: 1px solid #888; width: 90%; max-width: 500px; border-radius: 8px; }
706
- .modal-header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #e5e5e5; padding-bottom: 10px; margin-bottom: 15px; }
707
- .modal-header h2 { margin: 0; }
708
- .close { color: #aaa; font-size: 28px; font-weight: bold; cursor: pointer; }
709
  </style>
710
  </head>
711
  <body>
712
- <div class="container">
713
- <div class="header">
714
- <h1>Админ-панель</h1>
715
- <a href="{{ url_for('catalog') }}" class="button" style="margin-top:0;">В каталог</a>
716
- </div>
717
- {% with messages = get_flashed_messages(with_categories=true) %}
718
- {% if messages %}
719
- {% for category, message in messages %}
720
- <div class="message {{ category }}">{{ message }}</div>
721
- {% endfor %}
722
- {% endif %}
723
- {% endwith %}
724
- <div class="section">
725
- <h2>Управление категориями</h2>
726
- <form method="POST" style="display:flex; gap: 10px; align-items: flex-end;">
727
  <input type="hidden" name="action" value="add_category">
728
- <div style="flex-grow:1;"><label for="category_name">Новая категория:</label><input type="text" id="category_name" name="category_name" required></div>
729
- <button type="submit" style="margin-top:0;">Добавить</button>
730
  </form>
731
- </div>
732
- <div class="section">
733
- <h2>Товары</h2>
734
- <input type="search" id="admin-search" class="search-bar" placeholder="&#xF002; Поиск по всем товарам..." style="font-family: Arial, FontAwesome;">
735
  {% for category in categories %}
736
- <div class="category-block" data-category-name="{{ category }}">
737
- <div class="category-header">
738
- <h3>{{ category }}</h3>
739
- <div style="display:flex; align-items: center; gap: 10px;">
740
- <button class="button add-product-btn" data-category="{{ category }}" style="margin-top:0; padding: 8px 12px;">+ Добавить товар</button>
741
- <form method="POST" onsubmit="return confirm('Удалить категорию? Товары не будут удалены.');" style="margin:0;">
742
- <input type="hidden" name="action" value="delete_category">
743
- <input type="hidden" name="category_name" value="{{ category }}">
744
- <button type="submit" class="delete-btn" style="margin:0; padding: 8px 12px;"><i class="fas fa-trash"></i></button>
745
- </form>
746
- </div>
747
  </div>
748
- <div class="category-content">
749
- {% set products_in_cat = products_by_category.get(category, []) %}
750
- {% if products_in_cat %}
751
- {% for product in products_in_cat %}
752
- <div class="product-item" data-product-name="{{ product.name }}" data-product-desc="{{ product.description }}" data-product-id="{{ product.product_id }}" data-product-info='{{ product|tojson }}'>
753
- <img src="{% if product.photos %}{{ hf_url }}/photos/{{ product.photos[0] }}{% else %}https://via.placeholder.com/50x50.png?text=N/A{% endif %}" alt="{{ product.name }}">
754
- <div class="product-info">
755
- <strong>{{ product.name }}</strong><br>
756
- <small>{{ "%.0f"|format(product.price) }} {{ currency_code }}</small>
757
- </div>
758
- <div class="product-actions">
759
- <button class="button edit-btn edit-product-btn">Редактировать</button>
760
- <form method="POST" onsubmit="return confirm('Удалить товар?');" style="margin:0; display:inline-block;">
761
- <input type="hidden" name="action" value="delete_product">
762
- <input type="hidden" name="product_id" value="{{ product.product_id }}">
763
- <button type="submit" class="delete-btn">Удалить</button>
764
- </form>
765
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
766
  </div>
767
- {% endfor %}
768
- {% else %}
769
- <p>В этой категории нет товаров.</p>
770
- {% endif %}
 
771
  </div>
772
  </div>
773
  {% endfor %}
774
  </div>
775
- </div>
776
 
777
- <div id="productModal" class="modal">
778
- <div class="modal-content">
779
- <div class="modal-header">
780
- <h2 id="modalTitle">Добавить товар</h2>
781
- <span class="close">&times;</span>
782
- </div>
783
- <form id="productForm" method="POST" enctype="multipart/form-data">
784
- <input type="hidden" name="action" id="formAction" value="add_product">
785
- <input type="hidden" name="product_id" id="formProductId">
786
- <input type="hidden" name="category" id="formCategory">
787
- <label>Название:</label><input type="text" name="name" id="formName" required>
788
- <label>Цена:</label><input type="number" name="price" id="formPrice" step="0.01" required>
789
- <label>Описание:</label><textarea name="description" id="formDescription" rows="3"></textarea>
790
- <label>Категория:</label>
791
- <select name="new_category" id="formNewCategory" required>
792
- {% for category in categories %}
793
- <option value="{{ category }}">{{ category }}</option>
794
- {% endfor %}
795
- </select>
796
- <label>Фото (до 10):</label><input type="file" name="photos" multiple>
797
- <div id="existing-photos"></div>
798
- <button type="submit">Сохранить</button>
799
- </form>
800
- </div>
801
- </div>
802
-
803
- <script>
804
- const modal = document.getElementById('productModal');
805
- const closeModal = modal.querySelector('.close');
806
-
807
- closeModal.onclick = () => { modal.style.display = "none"; };
808
- window.onclick = (event) => { if (event.target == modal) { modal.style.display = "none"; } };
809
-
810
- document.querySelectorAll('.add-product-btn').forEach(button => {
811
- button.addEventListener('click', function() {
812
- const category = this.dataset.category;
813
- document.getElementById('modalTitle').innerText = 'Добавить товар в "' + category + '"';
814
- document.getElementById('productForm').reset();
815
- document.getElementById('formAction').value = 'add_product';
816
- document.getElementById('formProductId').value = '';
817
- document.getElementById('formCategory').value = category;
818
- document.getElementById('formNewCategory').value = category;
819
- document.getElementById('existing-photos').innerHTML = '';
820
- modal.style.display = 'block';
821
- });
822
- });
823
-
824
- document.querySelectorAll('.edit-product-btn').forEach(button => {
825
- button.addEventListener('click', function() {
826
- const productItem = this.closest('.product-item');
827
- const product = JSON.parse(productItem.dataset.productInfo);
828
-
829
- document.getElementById('modalTitle').innerText = 'Редактировать товар';
830
- document.getElementById('productForm').reset();
831
- document.getElementById('formAction').value = 'edit_product';
832
- document.getElementById('formProductId').value = product.product_id;
833
- document.getElementById('formName').value = product.name;
834
- document.getElementById('formPrice').value = product.price;
835
- document.getElementById('formDescription').value = product.description || '';
836
- document.getElementById('formNewCategory').value = product.category;
837
-
838
- const photosContainer = document.getElementById('existing-photos');
839
- photosContainer.innerHTML = '';
840
- if (product.photos && product.photos.length > 0) {
841
- let photoHtml = '<p>Текущие фото:</p>';
842
- product.photos.forEach(p => {
843
- photoHtml += `
844
- <div style="display: inline-block; margin-right: 10px;">
845
- <img src="${'{{ hf_url }}'}/photos/${p}" width="50" height="50">
846
- <input type="checkbox" name="delete_photos" value="${p}"> Удалить
847
- </div>`;
848
- });
849
- photosContainer.innerHTML = photoHtml;
850
- }
851
-
852
- modal.style.display = 'block';
853
- });
854
- });
855
-
856
- document.getElementById('admin-search').addEventListener('input', function(e) {
857
- const searchTerm = e.target.value.toLowerCase();
858
- document.querySelectorAll('.category-block').forEach(catBlock => {
859
- let hasVisibleProducts = false;
860
- catBlock.querySelectorAll('.product-item').forEach(card => {
861
- const name = card.dataset.productName.toLowerCase();
862
- const desc = card.dataset.productDesc.toLowerCase();
863
- if (name.includes(searchTerm) || desc.includes(searchTerm)) {
864
- card.style.display = 'flex';
865
- hasVisibleProducts = true;
866
- } else {
867
- card.style.display = 'none';
868
- }
869
- });
870
- const categoryName = catBlock.dataset.categoryName.toLowerCase();
871
- if (hasVisibleProducts || categoryName.includes(searchTerm)) {
872
- catBlock.style.display = 'block';
873
- } else {
874
- catBlock.style.display = 'none';
875
- }
876
- });
877
- });
878
- </script>
879
  </body>
880
  </html>
881
  '''
882
 
883
  @app.route('/')
884
  def catalog():
885
- data = load_data()
886
- products = data.get('products', [])
887
- categories = data.get('categories', [])
888
- categories_with_counts = {cat: 0 for cat in categories}
889
- for product in products:
890
- cat = product.get('category', 'Без категории')
891
- if cat in categories_with_counts:
892
- categories_with_counts[cat] += 1
893
- else:
894
- if cat not in categories:
895
- categories_with_counts[cat] = 1
896
- sorted_categories = dict(sorted(categories_with_counts.items()))
897
- return render_template_string(
898
- CATEGORY_PAGE_TEMPLATE,
899
- categories_with_counts=sorted_categories,
900
- currency_code=CURRENCY_CODE
901
- )
902
-
903
- @app.route('/category/<category_name>')
904
- def product_list(category_name):
905
- data = load_data()
906
- products_in_category = [p for p in data.get('products', []) if p.get('category') == category_name]
907
- return render_template_string(
908
- PRODUCT_PAGE_TEMPLATE,
909
- products=products_in_category,
910
- title=category_name,
911
- products_json=json.dumps(products_in_category),
912
- repo_id=REPO_ID,
913
- currency_code=CURRENCY_CODE
914
- )
915
-
916
- @app.route('/search')
917
- def search():
918
- query = request.args.get('q', '').lower()
919
  data = load_data()
920
  all_products = data.get('products', [])
921
- if not query:
922
- search_results = all_products
923
- else:
924
- search_results = [
925
- p for p in all_products
926
- if query in p.get('name', '').lower() or query in p.get('description', '').lower()
927
- ]
928
  return render_template_string(
929
- PRODUCT_PAGE_TEMPLATE,
930
- products=search_results,
931
- title=f"Поиск: {request.args.get('q', '')}",
932
- products_json=json.dumps(search_results),
933
  repo_id=REPO_ID,
934
  currency_code=CURRENCY_CODE
935
  )
@@ -937,176 +629,146 @@ def search():
937
  @app.route('/create_order', methods=['POST'])
938
  def create_order():
939
  order_data = request.get_json()
940
- if not order_data or 'cart' not in order_data or not order_data['cart']:
941
- return jsonify({"error": "Корзина пуста."}), 400
 
942
  cart_items = order_data['cart']
943
- total_price = 0
 
944
  processed_cart = []
945
  for item in cart_items:
946
- try:
947
- price = float(item['price'])
948
- quantity = int(item['quantity'])
949
- processed_cart.append({
950
- "product_id": item.get('product_id', 'N/A'),
951
- "name": item['name'],
952
- "price": price,
953
- "quantity": quantity,
954
- "color": item.get('color', 'N/A'),
955
- "photo": item.get('photo'),
956
- "photo_url": f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/photos/{item['photo']}" if item.get('photo') else "https://via.placeholder.com/60x60.png?text=N/A"
957
- })
958
- total_price += price * quantity
959
- except (ValueError, TypeError):
960
- return jsonify({"error": "Неверные данные в корзине."}), 400
961
- order_id = f"{datetime.now().strftime('%y%m%d')}-{uuid4().hex[:4]}"
962
- order_timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
963
  new_order = {
964
- "id": order_id, "created_at": order_timestamp, "cart": processed_cart,
965
- "total_price": round(total_price, 2), "status": "new"
 
 
966
  }
 
967
  data = load_data()
968
  data['orders'][order_id] = new_order
969
  save_data(data)
 
970
  return jsonify({"order_id": order_id}), 201
971
 
972
  @app.route('/order/<order_id>')
973
  def view_order(order_id):
974
  data = load_data()
975
  order = data.get('orders', {}).get(order_id)
976
- return render_template_string(ORDER_PAGE_TEMPLATE, order=order, currency_code=CURRENCY_CODE, whatsapp_number=WHATSAPP_NUMBER)
 
977
 
978
- def _handle_photo_upload(files, product_name):
979
- photos_list = []
980
- if not HF_TOKEN_WRITE:
981
- return photos_list
982
- api = HfApi()
983
- for photo in files[:10]:
984
- if photo and photo.filename:
985
- try:
986
- safe_name = secure_filename(product_name.replace(' ', '_'))[:50]
987
- ext = os.path.splitext(photo.filename)[1].lower()
988
- photo_filename = f"{safe_name}_{uuid4().hex[:8]}{ext}"
989
- api.upload_file(
990
- path_or_fileobj=photo.stream, path_in_repo=f"photos/{photo_filename}",
991
- repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE
992
- )
993
- photos_list.append(photo_filename)
994
- except Exception as e:
995
- flash(f"Ошибка загрузки фото {photo.filename}: {e}", 'error')
996
- return photos_list
997
 
998
  @app.route('/admin', methods=['GET', 'POST'])
999
  def admin():
1000
  data = load_data()
 
 
 
1001
  if request.method == 'POST':
1002
  action = request.form.get('action')
1003
-
1004
  if action == 'add_category':
1005
- category_name = request.form.get('category_name', '').strip()
1006
- if category_name and category_name not in data['categories']:
1007
- data['categories'].append(category_name)
 
1008
  save_data(data)
1009
- flash('Категория добавлена.', 'success')
1010
- else:
1011
- flash('Ошибка: Неверное имя или категория уже существует.', 'error')
1012
 
1013
  elif action == 'delete_category':
1014
- category_to_delete = request.form.get('category_name')
1015
- if category_to_delete in data['categories']:
1016
- data['categories'].remove(category_to_delete)
1017
- for p in data['products']:
1018
- if p.get('category') == category_to_delete:
1019
- p['category'] = 'Без категории'
1020
  save_data(data)
1021
- flash('Категория удалена.', 'success')
1022
 
1023
  elif action == 'add_product':
1024
  name = request.form.get('name', '').strip()
1025
- price_str = request.form.get('price', '').replace(',', '.')
1026
- if not name or not price_str:
1027
- flash("Название и цена обязательны.", 'error')
1028
- return redirect(url_for('admin'))
1029
- try:
1030
- price = round(float(price_str), 2)
1031
- except ValueError:
1032
- flash("Неверный формат цены.", 'error')
1033
- return redirect(url_for('admin'))
1034
-
1035
- photos_list = _handle_photo_upload(request.files.getlist('photos'), name)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1036
  new_product = {
1037
- 'product_id': uuid4().hex, 'name': name, 'price': price,
1038
- 'description': request.form.get('description', '').strip(),
1039
- 'category': request.form.get('category'), 'photos': photos_list
 
 
1040
  }
1041
- data['products'].append(new_product)
 
1042
  save_data(data)
1043
- flash('Товар добавлен.', 'success')
1044
-
1045
- elif action == 'edit_product':
1046
- product_id = request.form.get('product_id')
1047
- product_to_edit = next((p for p in data['products'] if p.get('product_id') == product_id), None)
1048
- if product_to_edit:
1049
- name = request.form.get('name', '').strip()
1050
- product_to_edit['name'] = name
1051
- product_to_edit['price'] = round(float(request.form.get('price', '0').replace(',', '.')), 2)
1052
- product_to_edit['description'] = request.form.get('description', '').strip()
1053
- product_to_edit['category'] = request.form.get('new_category')
1054
-
1055
- photos_to_delete = request.form.getlist('delete_photos')
1056
- if photos_to_delete and HF_TOKEN_WRITE:
1057
- try:
1058
- api = HfApi()
1059
- api.delete_files(repo_id=REPO_ID, paths_in_repo=[f"photos/{p}" for p in photos_to_delete], repo_type="dataset", token=HF_TOKEN_WRITE)
1060
- product_to_edit['photos'] = [p for p in product_to_edit.get('photos', []) if p not in photos_to_delete]
1061
- except Exception as e:
1062
- flash(f"Не удалось удалить некоторые фото с сервера: {e}", "error")
1063
-
1064
- new_photos = _handle_photo_upload(request.files.getlist('photos'), name)
1065
- product_to_edit['photos'].extend(new_photos)
1066
-
1067
- save_data(data)
1068
- flash('Товар обновлен.', 'success')
1069
- else:
1070
- flash('Товар для редактирования не найден.', 'error')
1071
 
1072
  elif action == 'delete_product':
1073
- product_id = request.form.get('product_id')
1074
- product_to_delete = next((p for p in data['products'] if p.get('product_id') == product_id), None)
1075
- if product_to_delete:
1076
- photos_to_delete = product_to_delete.get('photos', [])
1077
- if photos_to_delete and HF_TOKEN_WRITE:
1078
- try:
1079
- api = HfApi()
1080
- api.delete_files(repo_id=REPO_ID, paths_in_repo=[f"photos/{p}" for p in photos_to_delete], repo_type="dataset", token=HF_TOKEN_WRITE)
1081
- except Exception:
1082
- flash("Не удалось удалить фото с сервера.", "error")
1083
- data['products'] = [p for p in data['products'] if p.get('product_id') != product_id]
1084
- save_data(data)
1085
- flash('Товар удален.', 'success')
1086
 
1087
  return redirect(url_for('admin'))
1088
-
1089
- products_by_category = {}
1090
- for product in data.get('products', []):
1091
- category = product.get('category', 'Без категории')
1092
- if category not in products_by_category:
1093
- products_by_category[category] = []
1094
- products_by_category[category].append(product)
1095
 
1096
  return render_template_string(
1097
- ADMIN_PAGE_TEMPLATE,
1098
- categories=sorted(data.get('categories', [])),
1099
- products_by_category=products_by_category,
1100
  repo_id=REPO_ID,
1101
- currency_code=CURRENCY_CODE,
1102
- hf_url=f"https://huggingface.co/datasets/{REPO_ID}/resolve/main"
1103
  )
1104
 
 
 
 
 
 
 
 
 
 
 
1105
  if __name__ == '__main__':
1106
  download_db_from_hf()
1107
  load_data()
 
1108
  if HF_TOKEN_WRITE:
1109
- backup_thread = threading.Thread(target=periodic_backup, daemon=True)
1110
- backup_thread.start()
1111
  port = int(os.environ.get('PORT', 7860))
1112
- app.run(debug=False, host='0.0.0.0', port=port)
 
1
  import os
 
2
  import base64
3
  import json
 
4
  import threading
5
  import time
6
  from datetime import datetime
7
  from uuid import uuid4
8
 
9
+ from flask import Flask, render_template_string, request, redirect, url_for, flash, jsonify
 
 
10
  from huggingface_hub import HfApi, hf_hub_download
11
  from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError
12
  from werkzeug.utils import secure_filename
 
16
  load_dotenv()
17
 
18
  app = Flask(__name__)
19
+ app.secret_key = 'super_secret_key_store_app_123'
20
  DATA_FILE = 'data.json'
21
  SYNC_FILES = [DATA_FILE]
22
+
23
+ REPO_ID = os.getenv("REPO_ID", "Kgshop/aiexample")
24
  HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
25
  HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
 
 
 
 
 
26
 
27
+ WHATSAPP_NUMBER = "+996701202013"
28
+ CURRENCY_CODE = 'T'
29
 
30
+ def download_db_from_hf(specific_file=None, retries=3, delay=5):
 
 
31
  token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE
32
  files_to_download = [specific_file] if specific_file else SYNC_FILES
33
  all_successful = True
34
+
35
  for file_name in files_to_download:
36
  success = False
37
  for attempt in range(retries + 1):
38
  try:
39
+ hf_hub_download(
40
  repo_id=REPO_ID,
41
  filename=file_name,
42
  repo_type="dataset",
 
57
  if file_name == DATA_FILE:
58
  with open(file_name, 'w', encoding='utf-8') as f:
59
  json.dump({'products': [], 'categories': [], 'orders': {}}, f)
60
+ except Exception:
61
  pass
62
  success = False
63
  break
64
+ except requests.exceptions.RequestException:
65
+ pass
66
+ except Exception:
67
+ pass
68
+
 
69
  if attempt < retries:
70
  time.sleep(delay)
71
+
72
  if not success:
73
  all_successful = False
74
+
75
  return all_successful
76
 
77
  def upload_db_to_hf(specific_file=None):
 
80
  try:
81
  api = HfApi()
82
  files_to_upload = [specific_file] if specific_file else SYNC_FILES
83
+
84
  for file_name in files_to_upload:
85
  if os.path.exists(file_name):
86
  try:
 
92
  token=HF_TOKEN_WRITE,
93
  commit_message=f"Sync {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
94
  )
95
+ except Exception:
96
+ pass
97
+ except Exception:
 
 
98
  pass
99
 
100
  def periodic_backup():
 
101
  while True:
102
+ time.sleep(1800)
103
  upload_db_to_hf()
104
 
105
  def load_data():
 
109
  with open(DATA_FILE, 'r', encoding='utf-8') as file:
110
  data = json.load(file)
111
  if not isinstance(data, dict):
112
+ raise FileNotFoundError
113
  if 'products' not in data: data['products'] = []
114
  if 'categories' not in data: data['categories'] = []
115
  if 'orders' not in data: data['orders'] = {}
116
+ except (FileNotFoundError, json.JSONDecodeError):
117
  if download_db_from_hf(specific_file=DATA_FILE):
118
  try:
119
  with open(DATA_FILE, 'r', encoding='utf-8') as file:
120
  data = json.load(file)
 
 
121
  if 'products' not in data: data['products'] = []
122
  if 'categories' not in data: data['categories'] = []
123
  if 'orders' not in data: data['orders'] = {}
124
+ except Exception:
125
  data = default_data
126
  else:
127
  data = default_data
128
+ except Exception:
129
  data = default_data
130
+
131
  for product in data['products']:
132
  if 'product_id' not in product:
133
  product['product_id'] = uuid4().hex
134
+
 
135
  if not os.path.exists(DATA_FILE):
136
  try:
137
  with open(DATA_FILE, 'w', encoding='utf-8') as f:
138
  json.dump(default_data, f)
139
+ except Exception:
140
  pass
141
  return data
142
 
 
147
  if 'products' not in data: data['products'] = []
148
  if 'categories' not in data: data['categories'] = []
149
  if 'orders' not in data: data['orders'] = {}
150
+
151
  with open(DATA_FILE, 'w', encoding='utf-8') as file:
152
  json.dump(data, file, ensure_ascii=False, indent=4)
153
  upload_db_to_hf(specific_file=DATA_FILE)
154
+ except Exception:
155
  pass
156
 
157
+
158
+ CATALOG_TEMPLATE = '''
159
  <!DOCTYPE html>
160
  <html lang="ru">
161
  <head>
162
  <meta charset="UTF-8">
163
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
164
+ <title>Магазин</title>
165
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
166
  <style>
167
+ * { margin: 0; padding: 0; box-sizing: border-box; font-family: sans-serif; -webkit-tap-highlight-color: transparent; }
168
+ body { background-color: #f5f5f5; color: #333; }
169
+ .header { display: flex; align-items: center; justify-content: space-between; padding: 15px 20px; background: #fff; box-shadow: 0 2px 5px rgba(0,0,0,0.05); position: sticky; top: 0; z-index: 100; }
170
+ .header h1 { font-size: 1.5rem; font-weight: bold; }
171
+ .back-btn { display: none; font-size: 1.2rem; cursor: pointer; color: #333; margin-right: 15px; }
172
+ .search-bar { padding: 10px 20px; background: #fff; border-bottom: 1px solid #eee; display: flex; align-items: center; }
173
+ .search-bar input { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 8px; outline: none; font-size: 1rem; }
174
+
175
+ .categories-container { display: grid; grid-template-columns: 1fr 1fr; background: #fff; }
176
+ .category-item { padding: 15px 20px; border-bottom: 1px dashed #ccc; display: flex; justify-content: space-between; align-items: center; cursor: pointer; }
177
+ .category-item:nth-child(odd) { border-right: 1px dashed #ccc; }
178
+ .category-item span.name { font-size: 0.95rem; }
179
+ .category-item span.count { color: #999; font-size: 0.9rem; }
180
+
181
+ .products-container { display: none; padding: 15px; display: flex; flex-direction: column; gap: 15px; }
182
+ .product-card { background: #fff; border-radius: 12px; padding: 15px; display: flex; box-shadow: 0 2px 8px rgba(0,0,0,0.08); align-items: stretch; gap: 15px; }
183
+ .product-img { width: 100px; height: 100px; border-radius: 8px; object-fit: cover; flex-shrink: 0; border: 1px solid #eee;}
184
+ .product-info { flex-grow: 1; display: flex; flex-direction: column; justify-content: space-between; }
185
+ .product-title { font-size: 0.95rem; font-weight: bold; margin-bottom: 10px; line-height: 1.3; }
186
+ .product-bottom { display: flex; flex-direction: column; align-items: flex-end; gap: 10px; }
187
+ .product-price { font-weight: bold; font-size: 1rem; }
188
+ .quantity-control { display: flex; align-items: center; background: #f0f0f0; border-radius: 4px; overflow: hidden; }
189
+ .quantity-control button { border: none; background: #e0e0e0; width: 35px; height: 35px; font-size: 1.2rem; cursor: pointer; }
190
+ .quantity-control input { width: 40px; height: 35px; border: none; text-align: center; background: transparent; font-weight: bold; pointer-events: none; }
191
+
192
+ .social-buttons { position: fixed; left: 15px; bottom: 80px; display: flex; flex-direction: column; gap: 10px; z-index: 99; }
193
+ .social-btn { width: 45px; height: 45px; border-radius: 50%; color: #fff; display: flex; justify-content: center; align-items: center; font-size: 1.5rem; text-decoration: none; box-shadow: 0 2px 5px rgba(0,0,0,0.2); }
194
+ .btn-wa { background: #25D366; }
195
+ .btn-ig { background: #E1306C; }
196
+ .btn-tg { background: #0088cc; }
197
+
198
+ .cart-bar { position: fixed; bottom: 0; left: 0; width: 100%; background: #fff; box-shadow: 0 -2px 10px rgba(0,0,0,0.1); padding: 15px 20px; display: none; justify-content: space-between; align-items: center; z-index: 100; }
199
+ .cart-info { display: flex; flex-direction: column; }
200
+ .cart-total { font-size: 1.2rem; font-weight: bold; }
201
+ .checkout-btn { background: #333; color: #fff; padding: 12px 25px; border: none; border-radius: 8px; font-weight: bold; font-size: 1rem; cursor: pointer; }
202
+
203
+ .modal-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 200; justify-content: center; align-items: flex-end; }
204
+ .modal-content { background: #fff; width: 100%; max-height: 80vh; border-radius: 20px 20px 0 0; padding: 20px; overflow-y: auto; display: flex; flex-direction: column; }
205
+ .modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
206
+ .modal-close { font-size: 1.5rem; cursor: pointer; border: none; background: transparent; }
207
+ .cart-item-list { display: flex; flex-direction: column; gap: 15px; margin-bottom: 20px; }
208
+ .cart-item { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #eee; padding-bottom: 10px; }
209
+ .cart-item-name { flex-grow: 1; font-size: 0.9rem; padding-right: 10px; }
210
+ .confirm-btn { background: #25D366; color: #fff; width: 100%; padding: 15px; border: none; border-radius: 10px; font-size: 1.1rem; font-weight: bold; cursor: pointer; }
211
  </style>
212
  </head>
213
  <body>
214
  <div class="header">
215
+ <div style="display: flex; align-items: center;">
216
+ <i class="fas fa-chevron-left back-btn" id="backBtn" onclick="showCategories()"></i>
217
+ <h1 id="pageTitle">Каталог</h1>
 
 
 
 
 
 
 
 
 
 
 
 
218
  </div>
219
+ <i class="fas fa-bars" style="font-size: 1.5rem;"></i>
220
  </div>
221
+
222
+ <div class="search-bar" id="searchBar">
223
+ <input type="text" id="searchInput" placeholder="Поиск..." oninput="filterCategories()">
 
 
 
 
 
 
 
 
 
224
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
225
 
226
+ <div class="categories-container" id="categoriesContainer"></div>
227
+ <div class="products-container" id="productsContainer"></div>
 
228
 
229
+ <div class="social-buttons">
230
+ <a href="#" class="social-btn btn-wa"><i class="fab fa-whatsapp"></i></a>
231
+ <a href="#" class="social-btn btn-ig"><i class="fab fa-instagram"></i></a>
232
+ <a href="#" class="social-btn btn-tg"><i class="fab fa-telegram-plane"></i></a>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
  </div>
234
+
235
+ <div class="cart-bar" id="cartBar">
236
+ <div class="cart-info">
237
+ <span style="font-size: 0.8rem; color: #666;">Итого:</span>
238
+ <span class="cart-total"><span id="cartTotalSum">0</span> {{ currency_code }}</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
  </div>
240
+ <button class="checkout-btn" onclick="openCartModal()">Оформить</button>
241
  </div>
242
 
243
+ <div class="modal-overlay" id="cartModal">
244
+ <div class="modal-content">
245
+ <div class="modal-header">
246
+ <h2>Корзина</h2>
247
+ <button class="modal-close" onclick="closeCartModal()">&times;</button>
248
+ </div>
249
+ <div class="cart-item-list" id="cartItemList"></div>
250
+ <button class="confirm-btn" onclick="submitOrder()">Сформировать заказ</button>
 
 
 
 
 
 
 
 
 
 
 
251
  </div>
252
  </div>
253
 
254
  <script>
255
+ const products = {{ products_json|safe }};
256
+ const categoriesList = {{ categories_json|safe }};
257
  const repoId = '{{ repo_id }}';
258
+ const currency = '{{ currency_code }}';
259
+
260
+ let cart = {};
261
+
262
+ function init() {
263
+ renderCategories();
264
+ updateCartUI();
265
+ }
266
+
267
+ function renderCategories(filter = '') {
268
+ const container = document.getElementById('categoriesContainer');
269
+ document.getElementById('productsContainer').style.display = 'none';
270
+ container.style.display = 'grid';
271
+ document.getElementById('backBtn').style.display = 'none';
272
+ document.getElementById('pageTitle').innerText = 'Каталог';
273
+
274
+ container.innerHTML = '';
275
+
276
+ categoriesList.forEach(cat => {
277
+ if(filter && !cat.toLowerCase().includes(filter.toLowerCase())) return;
278
+
279
+ const catProducts = products.filter(p => p.category === cat);
280
+ const count = catProducts.length;
281
+
282
+ const div = document.createElement('div');
283
+ div.className = 'category-item';
284
+ div.onclick = () => showProducts(cat);
285
+ div.innerHTML = `<span class="name">${cat}</span><span class="count">${count}</span>`;
286
+ container.appendChild(div);
287
+ });
288
+ }
289
+
290
+ function showCategories() {
291
+ document.getElementById('searchInput').value = '';
292
+ renderCategories();
293
+ }
294
 
295
+ function filterCategories() {
296
+ renderCategories(document.getElementById('searchInput').value);
297
+ }
298
+
299
+ function showProducts(category) {
300
+ document.getElementById('categoriesContainer').style.display = 'none';
301
+ const container = document.getElementById('productsContainer');
302
+ container.style.display = 'flex';
303
+ document.getElementById('backBtn').style.display = 'block';
304
+ document.getElementById('pageTitle').innerText = category;
305
+
306
+ container.innerHTML = '';
307
+
308
+ const catProducts = products.filter(p => p.category === category);
309
+
310
+ catProducts.forEach(p => {
311
+ const qty = cart[p.product_id] ? cart[p.product_id].quantity : 0;
312
+ const photoUrl = p.photos && p.photos.length > 0
313
+ ? `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${p.photos[0]}`
314
+ : 'https://via.placeholder.com/100';
315
+
316
+ const div = document.createElement('div');
317
+ div.className = 'product-card';
318
+ div.innerHTML = `
319
+ <img src="${photoUrl}" class="product-img">
320
+ <div class="product-info">
321
+ <div class="product-title">${p.name}</div>
322
+ <div class="product-bottom">
323
+ <div class="product-price">${p.price} ${currency}</div>
324
+ <div class="quantity-control">
325
+ <button onclick="updateCart('${p.product_id}', -1)">-</button>
326
+ <input type="text" id="qty-${p.product_id}" value="${qty}" readonly>
327
+ <button onclick="updateCart('${p.product_id}', 1)">+</button>
328
+ </div>
329
+ </div>
330
+ </div>
331
+ `;
332
+ container.appendChild(div);
333
+ });
334
  }
335
 
336
  function updateCart(productId, change) {
337
+ const product = products.find(p => p.product_id === productId);
338
  if (!product) return;
339
+
340
+ if (!cart[productId]) {
341
+ cart[productId] = { ...product, quantity: 0 };
 
 
 
342
  }
343
+
344
+ cart[productId].quantity += change;
345
+
346
+ if (cart[productId].quantity <= 0) {
347
+ delete cart[productId];
348
+ document.getElementById(`qty-${productId}`).value = 0;
349
+ } else {
350
+ document.getElementById(`qty-${productId}`).value = cart[productId].quantity;
351
  }
352
+
353
+ updateCartUI();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
354
  }
355
 
356
+ function updateCartUI() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
357
  let total = 0;
358
+ for (let id in cart) {
359
+ total += cart[id].price * cart[id].quantity;
360
+ }
361
+
362
+ const cartBar = document.getElementById('cartBar');
363
+ if (total > 0) {
364
+ cartBar.style.display = 'flex';
365
+ document.getElementById('cartTotalSum').innerText = total;
366
  } else {
367
+ cartBar.style.display = 'none';
368
+ closeCartModal();
 
 
 
 
369
  }
370
  }
371
 
372
+ function openCartModal() {
373
+ const list = document.getElementById('cartItemList');
374
+ list.innerHTML = '';
375
+
376
+ for (let id in cart) {
377
+ const item = cart[id];
378
+ list.innerHTML += `
379
+ <div class="cart-item">
380
+ <div class="cart-item-name">${item.name}</div>
381
+ <div style="font-weight: bold; white-space: nowrap;">${item.quantity} x ${item.price} ${currency}</div>
382
+ </div>
383
+ `;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
384
  }
385
+ document.getElementById('cartModal').style.display = 'flex';
386
  }
387
+
388
+ function closeCartModal() {
389
+ document.getElementById('cartModal').style.display = 'none';
390
+ }
391
+
392
+ function submitOrder() {
393
+ const cartArray = Object.values(cart);
394
+ fetch('/create_order', {
395
+ method: 'POST',
396
+ headers: { 'Content-Type': 'application/json' },
397
+ body: JSON.stringify({ cart: cartArray })
398
+ })
399
+ .then(r => r.json())
400
+ .then(data => {
401
+ if(data.order_id) {
402
+ cart = {};
403
+ window.location.href = `/order/${data.order_id}`;
404
  }
405
+ });
 
 
 
 
 
 
 
 
 
 
 
 
406
  }
407
 
408
+ init();
409
  </script>
410
  </body>
411
  </html>
412
  '''
413
 
414
+ ORDER_TEMPLATE = '''
415
  <!DOCTYPE html>
416
  <html lang="ru">
417
  <head>
418
  <meta charset="UTF-8">
419
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
420
  <title>Накладная №{{ order.id }}</title>
 
421
  <style>
422
+ * { box-sizing: border-box; font-family: 'Times New Roman', serif; }
423
+ body { margin: 0; padding: 20px; background: #f0f0f0; display: flex; flex-direction: column; align-items: center; }
424
+ .invoice-box { background: #fff; width: 100%; max-width: 900px; padding: 40px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }
425
+ .header { display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: 30px; border-bottom: 2px solid #000; padding-bottom: 10px; }
426
+ .header h1 { margin: 0; font-size: 32px; font-weight: bold; }
427
+ .info-row { display: flex; justify-content: space-between; margin-bottom: 20px; font-size: 18px; font-weight: bold; }
428
+ table { width: 100%; border-collapse: collapse; margin-bottom: 20px; }
429
+ th, td { border: 1px solid #000; padding: 8px; text-align: center; font-size: 16px; font-weight: bold; }
430
+ th { background: #f9f9f9; }
431
+ .img-cell img { width: 50px; height: 50px; object-fit: cover; }
432
+ .total-row td { font-size: 18px; }
433
+
434
+ .floating-buttons { position: fixed; bottom: 30px; right: 30px; display: flex; flex-direction: column; gap: 15px; }
435
+ .btn { padding: 15px; border-radius: 50px; border: none; font-size: 16px; font-weight: bold; cursor: pointer; color: #fff; display: flex; align-items: center; justify-content: center; gap: 10px; text-decoration: none; box-shadow: 0 4px 6px rgba(0,0,0,0.2); }
436
+ .btn-wa { background: #25D366; }
437
+ .btn-print { background: #333; }
438
+
439
  @media print {
440
+ body { background: #fff; padding: 0; }
441
+ .invoice-box { box-shadow: none; padding: 0; max-width: 100%; }
442
  .floating-buttons { display: none; }
443
  }
444
  </style>
445
  </head>
446
  <body>
 
447
  <div class="invoice-box">
448
+ <div class="header">
449
+ <div>
450
+ <span style="font-size: 24px; font-weight: bold;"></span>
451
+ </div>
452
  <h1>Накладная</h1>
453
+ <div>
454
+ <span style="font-size: 16px;">1 | 1</span>
 
 
455
  </div>
456
  </div>
457
+
458
+ <div class="info-row">
459
+ <div>NO: {{ order.id }}</div>
460
+ <div>дата: {{ order.created_at.split(' ')[0] }}</div>
461
+ </div>
462
+
463
+ <div class="info-row">
464
+ <div>покупатель: _________________</div>
465
+ </div>
466
+
467
+ <table>
468
  <thead>
469
  <tr>
470
  <th>NO</th>
471
  <th>Наименование</th>
472
  <th>Фото</th>
473
+ <th>кол-во</th>
474
+ <th>Цена</th>
475
+ <th>Сумма</th>
476
  </tr>
477
  </thead>
478
  <tbody>
479
  {% for item in order.cart %}
480
  <tr>
481
+ <td>{{ loop.index }}</td>
482
+ <td style="text-align: left;">{{ item.name }}</td>
483
+ <td class="img-cell"><img src="{{ item.photo_url }}" alt="img"></td>
484
+ <td>{{ item.quantity }}</td>
485
+ <td>{{ item.price }}</td>
486
+ <td>{{ item.price * item.quantity }}</td>
487
  </tr>
488
  {% endfor %}
 
 
489
  <tr class="total-row">
490
+ <td colspan="5" style="text-align: left;">Итого</td>
491
+ <td>{{ order.total_price }}</td>
492
  </tr>
493
+ </tbody>
494
  </table>
495
  </div>
496
+
497
  <div class="floating-buttons">
498
+ <button class="btn btn-print" onclick="window.print()">Печать</button>
499
+ <button class="btn btn-wa" onclick="sendToWA()">Отправить в WhatsApp</button>
500
  </div>
501
+
502
  <script>
503
+ function sendToWA() {
504
+ let msg = `Заказ №{{ order.id }}\n\n`;
 
 
 
505
  {% for item in order.cart %}
506
+ msg += `- {{ item.name }} x{{ item.quantity }} = {{ item.price * item.quantity }}\n`;
 
 
 
507
  {% endfor %}
508
+ msg += `\nИтого: {{ order.total_price }} {{ currency_code }}`;
509
+
510
+ window.open(`https://api.whatsapp.com/send?phone={{ whatsapp_number }}&text=${encodeURIComponent(msg)}`, '_blank');
511
  }
512
  </script>
 
 
 
 
 
 
 
513
  </body>
514
  </html>
515
  '''
516
 
517
+ ADMIN_TEMPLATE = '''
518
  <!DOCTYPE html>
519
  <html lang="ru">
520
  <head>
521
  <meta charset="UTF-8">
522
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
523
  <title>Админ-панель</title>
 
524
  <style>
525
+ * { box-sizing: border-box; font-family: sans-serif; }
526
+ body { background: #f4f6f9; padding: 20px; }
527
+ .container { max-width: 1000px; margin: 0 auto; background: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
528
+ h1, h2 { color: #333; }
529
+ .form-group { margin-bottom: 15px; }
530
+ input[type="text"], input[type="number"], select { width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 4px; }
531
+ button { padding: 10px 15px; border: none; border-radius: 4px; background: #333; color: #fff; cursor: pointer; }
532
+ button.danger { background: #dc3545; }
533
+ .category-block { border: 1px solid #ddd; margin-bottom: 10px; border-radius: 4px; }
534
+ .category-header { background: #f8f9fa; padding: 15px; cursor: pointer; font-weight: bold; display: flex; justify-content: space-between; align-items: center; }
535
+ .category-content { padding: 15px; display: none; border-top: 1px solid #ddd; }
536
+ .product-item { display: flex; justify-content: space-between; align-items: center; padding: 10px 0; border-bottom: 1px solid #eee; }
537
+ .product-item img { width: 40px; height: 40px; object-fit: cover; margin-right: 10px; border-radius: 4px; }
538
+ .add-product-form { background: #fdfdfd; padding: 15px; border: 1px dashed #ccc; margin-top: 15px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
539
  </style>
540
  </head>
541
  <body>
542
+ <div class="container">
543
+ <div style="display: flex; justify-content: space-between; align-items: center;">
544
+ <h1>Админ-панель</h1>
545
+ <a href="/" style="text-decoration: none; background: #007bff; color: white; padding: 10px 15px; border-radius: 4px;">В каталог</a>
546
+ </div>
547
+
548
+ <div style="margin-bottom: 20px; display: flex; gap: 10px;">
549
+ <form method="POST" action="/force_upload"><button type="submit" style="background:#28a745;">Сохранить БД на сервер</button></form>
550
+ <form method="POST" action="/force_download"><button type="submit" style="background:#17a2b8;">Скачать БД с сервера</button></form>
551
+ </div>
552
+
553
+ <h2>Категории и Товары</h2>
554
+ <form method="POST" style="margin-bottom: 20px; display: flex; gap: 10px;">
 
 
555
  <input type="hidden" name="action" value="add_category">
556
+ <input type="text" name="category_name" placeholder="Новая категория" required>
557
+ <button type="submit">Добавить категорию</button>
558
  </form>
559
+
 
 
 
560
  {% for category in categories %}
561
+ <div class="category-block">
562
+ <div class="category-header" onclick="toggleCategory('{{ loop.index }}')">
563
+ <span>{{ category }}</span>
564
+ <form method="POST" style="margin:0;" onsubmit="return confirm('Удалить категорию?');">
565
+ <input type="hidden" name="action" value="delete_category">
566
+ <input type="hidden" name="category_name" value="{{ category }}">
567
+ <button type="submit" class="danger">Удалить</button>
568
+ </form>
 
 
 
569
  </div>
570
+ <div class="category-content" id="cat-{{ loop.index }}">
571
+
572
+ {% for product in products %}
573
+ {% if product.category == category %}
574
+ <div class="product-item">
575
+ <div style="display: flex; align-items: center;">
576
+ <img src="{{ 'https://huggingface.co/datasets/' + repo_id + '/resolve/main/photos/' + product.photos[0] if product.photos else 'https://via.placeholder.com/40' }}">
577
+ <span>{{ product.name }} - {{ product.price }} {{ currency_code }}</span>
 
 
 
 
 
 
 
 
 
578
  </div>
579
+ <form method="POST" style="margin:0;" onsubmit="return confirm('Удалить товар?');">
580
+ <input type="hidden" name="action" value="delete_product">
581
+ <input type="hidden" name="product_id" value="{{ product.product_id }}">
582
+ <button type="submit" class="danger">Удалить</button>
583
+ </form>
584
+ </div>
585
+ {% endif %}
586
+ {% endfor %}
587
+
588
+ <form class="add-product-form" method="POST" enctype="multipart/form-data">
589
+ <input type="hidden" name="action" value="add_product">
590
+ <input type="hidden" name="category" value="{{ category }}">
591
+ <div style="display: flex; gap: 10px; margin-bottom: 10px;">
592
+ <input type="text" name="name" placeholder="Название товара" required style="flex:2;">
593
+ <input type="number" name="price" placeholder="Цена" required style="flex:1;">
594
  </div>
595
+ <div style="margin-bottom: 10px;">
596
+ <input type="file" name="photo" accept="image/*" required>
597
+ </div>
598
+ <button type="submit" style="background: #28a745;">Добавить товар в "{{ category }}"</button>
599
+ </form>
600
  </div>
601
  </div>
602
  {% endfor %}
603
  </div>
 
604
 
605
+ <script>
606
+ function toggleCategory(id) {
607
+ const el = document.getElementById('cat-' + id);
608
+ el.style.display = el.style.display === 'block' ? 'none' : 'block';
609
+ }
610
+ </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
611
  </body>
612
  </html>
613
  '''
614
 
615
  @app.route('/')
616
  def catalog():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
617
  data = load_data()
618
  all_products = data.get('products', [])
619
+ categories = data.get('categories', [])
620
+
 
 
 
 
 
621
  return render_template_string(
622
+ CATALOG_TEMPLATE,
623
+ products_json=json.dumps(all_products),
624
+ categories_json=json.dumps(categories),
 
625
  repo_id=REPO_ID,
626
  currency_code=CURRENCY_CODE
627
  )
 
629
  @app.route('/create_order', methods=['POST'])
630
  def create_order():
631
  order_data = request.get_json()
632
+ if not order_data or 'cart' not in order_data:
633
+ return jsonify({"error": "Bad request"}), 400
634
+
635
  cart_items = order_data['cart']
636
+ total_price = sum(float(item['price']) * int(item['quantity']) for item in cart_items)
637
+
638
  processed_cart = []
639
  for item in cart_items:
640
+ processed_cart.append({
641
+ "name": item['name'],
642
+ "price": float(item['price']),
643
+ "quantity": int(item['quantity']),
644
+ "photo_url": f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/photos/{item['photos'][0]}" if item.get('photos') else "https://via.placeholder.com/50"
645
+ })
646
+
647
+ order_id = f"SA-{datetime.now().strftime('%Y%m%d')}-{str(len(load_data().get('orders', {}))+1).zfill(3)}"
648
+
 
 
 
 
 
 
 
 
649
  new_order = {
650
+ "id": order_id,
651
+ "created_at": datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
652
+ "cart": processed_cart,
653
+ "total_price": total_price
654
  }
655
+
656
  data = load_data()
657
  data['orders'][order_id] = new_order
658
  save_data(data)
659
+
660
  return jsonify({"order_id": order_id}), 201
661
 
662
  @app.route('/order/<order_id>')
663
  def view_order(order_id):
664
  data = load_data()
665
  order = data.get('orders', {}).get(order_id)
666
+ if not order:
667
+ return "Order not found", 404
668
 
669
+ return render_template_string(
670
+ ORDER_TEMPLATE,
671
+ order=order,
672
+ whatsapp_number=WHATSAPP_NUMBER,
673
+ currency_code=CURRENCY_CODE
674
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
675
 
676
  @app.route('/admin', methods=['GET', 'POST'])
677
  def admin():
678
  data = load_data()
679
+ products = data.get('products', [])
680
+ categories = data.get('categories', [])
681
+
682
  if request.method == 'POST':
683
  action = request.form.get('action')
684
+
685
  if action == 'add_category':
686
+ cat_name = request.form.get('category_name', '').strip()
687
+ if cat_name and cat_name not in categories:
688
+ categories.append(cat_name)
689
+ data['categories'] = categories
690
  save_data(data)
 
 
 
691
 
692
  elif action == 'delete_category':
693
+ cat_name = request.form.get('category_name')
694
+ if cat_name in categories:
695
+ categories.remove(cat_name)
696
+ data['products'] = [p for p in products if p.get('category') != cat_name]
697
+ data['categories'] = categories
 
698
  save_data(data)
 
699
 
700
  elif action == 'add_product':
701
  name = request.form.get('name', '').strip()
702
+ price = float(request.form.get('price', 0))
703
+ category = request.form.get('category')
704
+ photo = request.files.get('photo')
705
+
706
+ photos_list = []
707
+ if photo and photo.filename and HF_TOKEN_WRITE:
708
+ uploads_dir = 'uploads_temp'
709
+ os.makedirs(uploads_dir, exist_ok=True)
710
+ ext = os.path.splitext(photo.filename)[1].lower()
711
+ photo_filename = f"{uuid4().hex}{ext}"
712
+ temp_path = os.path.join(uploads_dir, photo_filename)
713
+ photo.save(temp_path)
714
+ try:
715
+ api = HfApi()
716
+ api.upload_file(
717
+ path_or_fileobj=temp_path,
718
+ path_in_repo=f"photos/{photo_filename}",
719
+ repo_id=REPO_ID,
720
+ repo_type="dataset",
721
+ token=HF_TOKEN_WRITE
722
+ )
723
+ photos_list.append(photo_filename)
724
+ except Exception:
725
+ pass
726
+ finally:
727
+ if os.path.exists(temp_path):
728
+ os.remove(temp_path)
729
+
730
  new_product = {
731
+ 'product_id': uuid4().hex,
732
+ 'name': name,
733
+ 'price': price,
734
+ 'category': category,
735
+ 'photos': photos_list
736
  }
737
+ products.append(new_product)
738
+ data['products'] = products
739
  save_data(data)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
740
 
741
  elif action == 'delete_product':
742
+ pid = request.form.get('product_id')
743
+ data['products'] = [p for p in products if p.get('product_id') != pid]
744
+ save_data(data)
 
 
 
 
 
 
 
 
 
 
745
 
746
  return redirect(url_for('admin'))
 
 
 
 
 
 
 
747
 
748
  return render_template_string(
749
+ ADMIN_TEMPLATE,
750
+ products=products,
751
+ categories=categories,
752
  repo_id=REPO_ID,
753
+ currency_code=CURRENCY_CODE
 
754
  )
755
 
756
+ @app.route('/force_upload', methods=['POST'])
757
+ def force_upload():
758
+ upload_db_to_hf()
759
+ return redirect(url_for('admin'))
760
+
761
+ @app.route('/force_download', methods=['POST'])
762
+ def force_download():
763
+ download_db_from_hf()
764
+ return redirect(url_for('admin'))
765
+
766
  if __name__ == '__main__':
767
  download_db_from_hf()
768
  load_data()
769
+
770
  if HF_TOKEN_WRITE:
771
+ threading.Thread(target=periodic_backup, daemon=True).start()
772
+
773
  port = int(os.environ.get('PORT', 7860))
774
+ app.run(host='0.0.0.0', port=port)